1- # Firefly Framework - Cache PostgreSQL Adapter
1+ # Firefly Framework - Cache - Postgres
22
33[ ![ CI] ( https://github.com/fireflyframework/fireflyframework-cache-postgres/actions/workflows/ci.yml/badge.svg )] ( https://github.com/fireflyframework/fireflyframework-cache-postgres/actions/workflows/ci.yml )
44[ ![ License] ( https://img.shields.io/badge/License-Apache%202.0-blue.svg )] ( LICENSE )
55[ ![ Java] ( https://img.shields.io/badge/Java-21%2B-orange.svg )] ( https://openjdk.org )
66[ ![ Spring Boot] ( https://img.shields.io/badge/Spring%20Boot-3.x-green.svg )] ( https://spring.io/projects/spring-boot )
77
8- > Reactive, durable PostgreSQL/R2DBC cache provider adapter for the Firefly Framework cache abstraction — a SQL-backed, non-blocking ` FireflyCache ` implementation with TTL expiration and background cleanup .
8+ > Durable PostgreSQL/R2DBC cache provider adapter for the Firefly Framework cache abstraction — a fully reactive, SQL-backed ` CacheAdapter ` with TTL expiry, atomic put-if-absent, and prefix eviction .
99
1010---
1111
2525## Overview
2626
2727` fireflyframework-cache-postgres ` is one of the pluggable cache ** provider adapters** for the Firefly
28- Framework cache abstraction. It implements the cache SPI defined in
29- [ ` fireflyframework-cache-core ` ] ( https://github.com/fireflyframework/fireflyframework-cache-core )
30- (` FireflyCache ` , ` CacheProvider ` , ` CacheType ` ) on top of ** PostgreSQL** using
31- ** R2DBC** for fully reactive, non-blocking database access.
28+ Framework cache abstraction. It implements the ` CacheAdapter ` port defined in
29+ [ ` fireflyframework-cache ` ] ( https://github.com/fireflyframework/fireflyframework-cache ) (the cache core)
30+ using ** PostgreSQL** as a persistent, distributed cache backend over fully reactive ** R2DBC** .
3231
3332The cache core ships ** Caffeine** as the in-process default. Every other backend — Redis, Hazelcast,
34- JCache and PostgreSQL — lives in its own adapter module so applications only pull in the dependency
35- they actually use. This module is the PostgreSQL adapter: dropping it on the classpath and selecting
36- ` firefly.cache.provider=postgres ` routes the unified cache API to a relational table instead of an
37- in-memory store.
33+ JCache and PostgreSQL — lives in its own adapter module, so an application only pulls in the
34+ dependency it actually uses. This module contributes a ` PostgresProvider ` (discovered through the
35+ ` CacheProviderFactory ` ` ServiceLoader ` SPI) and a ` PostgresCacheAutoConfiguration ` . Once on the
36+ classpath with ` firefly.cache.postgres.enabled=true ` , the cache core makes the ` POSTGRES ` cache type
37+ available to ` FireflyCacheManager ` and selects it when ` firefly.cache.default-cache-type=POSTGRES `
38+ (or via ` AUTO ` ).
3839
3940Use this adapter when you want a ** durable, SQL-backed cache** rather than an in-memory or Redis
40- cache — for example, when entries must survive process restarts, when you want cache contents to be
41- queryable/auditable through ordinary SQL, or when you would rather reuse your existing PostgreSQL
42- infrastructure than operate a separate cache cluster. Because it is built on R2DBC and Project
43- Reactor, it integrates cleanly with reactive Spring WebFlux stacks and the rest of the Firefly
44- Framework without introducing blocking calls.
41+ cache — for example, when entries must survive process restarts, when cache contents should be
42+ queryable and auditable through ordinary SQL, or when you would rather reuse your existing PostgreSQL
43+ infrastructure than operate a separate cache cluster. Cache entries live in a single relational table
44+ keyed by ` (cache_name, cache_key) ` , with values serialized to JSON and stored in a ` BYTEA ` column;
45+ time-to-live is enforced through an ` expires_at ` timestamp checked lazily on every read. Because the
46+ adapter is built on R2DBC and Project Reactor, every operation is non-blocking and integrates cleanly
47+ with Spring WebFlux and the rest of the Firefly Framework.
4548
4649### Where it sits in the framework
4750
4851| Module | Role |
4952| --- | --- |
50- | ` fireflyframework-cache-core ` | Cache SPI + Caffeine default (` FireflyCache ` , ` CacheProvider ` , ` CacheManager ` , ` CacheType ` ) |
53+ | ` fireflyframework-cache ` | Cache abstraction + SPI + Caffeine default (` CacheAdapter ` , ` CacheType ` , ` CacheProviderFactory ` , ` FireflyCacheManager ` ) |
5154| ` fireflyframework-cache-redis ` | Redis provider adapter |
5255| ` fireflyframework-cache-hazelcast ` | Hazelcast provider adapter |
5356| ` fireflyframework-cache-jcache ` | JCache (JSR-107) provider adapter |
5457| ** ` fireflyframework-cache-postgres ` ** | ** PostgreSQL / R2DBC provider adapter (this module)** |
5558
5659## Features
5760
58- - ** Reactive end to end** — implements ` FireflyCache ` over R2DBC; no blocking JDBC calls, suitable for Spring WebFlux and reactive Firefly services.
59- - ** Durable, SQL-backed storage** — cache entries persist in a relational table (` firefly_cache ` by default) so they survive restarts and remain queryable with plain SQL.
60- - ** Reports as ` CacheType.POSTGRES ` ** — slots into the cache abstraction alongside Caffeine, Redis, Hazelcast and JCache via the same ` CacheProvider ` SPI.
61- - ** TTL-based expiration** — per-entry time-to-live with a configurable default; zero/negative means no expiration.
62- - ** Background cleanup** — a periodic purge job removes expired rows on a configurable interval, keeping the table lean.
63- - ** Connection pooling** — uses ` r2dbc-pool ` for efficient, bounded reactive connection management.
64- - ** Jackson serialization** — values are serialized via ` jackson-databind ` for compact, portable storage.
65- - ** Spring Boot auto-configuration** — ` PostgresCacheAutoConfiguration ` wires everything automatically; gated by ` @ConditionalOnClass(CacheProvider.class) ` and the ` firefly.cache.postgres.enabled ` property.
66- - ** Type-safe configuration** — all settings exposed through ` PostgresCacheProperties ` under the ` firefly.cache.postgres ` prefix.
61+ - Full ` CacheAdapter ` implementation (` PostgresCacheAdapter ` ) backed by PostgreSQL R2DBC.
62+ - Reactive, non-blocking operations with per-operation connection lifecycle via ` Mono.usingWhen ` .
63+ - TTL support with lazy expiry on read (` expires_at IS NULL OR expires_at > NOW() ` ) and write-time ` expires_at = NOW() + ttl ` .
64+ - JSON serialization to a ` BYTEA ` column through the framework ` CacheSerializer ` (` JsonCacheSerializer ` ).
65+ - Atomic ` putIfAbsent ` using ` INSERT ... ON CONFLICT DO NOTHING ` ; upserts via ` ON CONFLICT DO UPDATE ` .
66+ - Native prefix eviction (` evictByPrefix ` ) via ` DELETE ... WHERE cache_key LIKE prefix || '%' ` .
67+ - Per-cache key namespacing (` <keyPrefix>:<cacheName>:<key> ` ) so one table hosts many named caches.
68+ - Pooled connections via ` r2dbc-pool ` with configurable min/max pool sizing.
69+ - Optional automatic schema creation (cache table + partial expiry index) on startup.
70+ - Connection health checks via ` Connection.validate(ValidationDepth.REMOTE) ` , surfaced as ` CacheHealth ` .
71+ - In-process statistics (requests, hits, misses, puts, evictions, entry count) as ` CacheStats ` .
72+ - ` ServiceLoader ` SPI registration (` PostgresProvider ` , ` CacheType.POSTGRES ` , priority 15) plus Spring Boot auto-configuration.
6773
6874## Requirements
6975
7076- Java 21+ (Java 25 recommended)
7177- Spring Boot 3.x
7278- Maven 3.9+
73- - A reachable PostgreSQL database (any supported version) with R2DBC connectivity
74- - ` fireflyframework-cache-core ` on the classpath (pulled in transitively as a dependency)
79+ - PostgreSQL 12+ reachable over R2DBC
80+ - ` fireflyframework-cache ` on the classpath (pulled in transitively as a dependency)
7581
7682## Installation
7783
78- Add the adapter to your application. The version is managed by the Firefly Framework
79- parent POM / BOM, so you normally omit ` <version> ` :
84+ Add the adapter to your application. The version is managed by the Firefly Framework parent POM / BOM,
85+ so you normally omit ` <version> ` :
8086
8187``` xml
8288<dependency >
@@ -85,8 +91,8 @@ parent POM / BOM, so you normally omit `<version>`:
8591</dependency >
8692```
8793
88- This brings in ` fireflyframework-cache-core ` (the cache SPI ), the ` r2dbc-postgresql ` driver,
89- ` r2dbc-pool ` and ` jackson-databind ` transitively.
94+ This brings in ` fireflyframework-cache ` (the cache abstraction ), the ` r2dbc-postgresql ` driver,
95+ ` r2dbc-pool ` and the R2DBC SPI transitively.
9096
9197If you are not inheriting the Firefly parent, import the BOM in your ` dependencyManagement ` and let it
9298manage the version:
@@ -107,99 +113,162 @@ manage the version:
107113
108114## Quick Start
109115
110- The adapter is auto-configured. To activate the PostgreSQL backend :
116+ 1 . ** Add the dependencies ** — the cache core and this adapter :
111117
112- 1 . ** Add the dependency** (see [ Installation] ( #installation ) ).
113- 2 . ** Select the provider** and point it at your database in ` application.yml ` :
118+ ``` xml
119+ <dependencies >
120+ <dependency >
121+ <groupId >org.fireflyframework</groupId >
122+ <artifactId >fireflyframework-cache</artifactId >
123+ </dependency >
124+ <dependency >
125+ <groupId >org.fireflyframework</groupId >
126+ <artifactId >fireflyframework-cache-postgres</artifactId >
127+ </dependency >
128+ </dependencies >
129+ ```
130+
131+ 2 . ** Enable the provider and point it at your database** in ` application.yml ` :
114132
115133``` yaml
116134firefly :
117135 cache :
118- provider : postgres # route the unified cache abstraction to this adapter
136+ default-cache-type : POSTGRES # select this adapter (or AUTO to let the core pick)
119137 postgres :
120- url : r2dbc:postgresql://localhost:5432/mydb
121- username : firefly
122- password : ${DB_PASSWORD}
123- table-name : firefly_cache
124- default-ttl : 10m
125- cleanup-interval : 5m
138+ enabled : true
139+ host : localhost
140+ port : 5432
141+ database : appdb
142+ username : app
143+ password : secret
126144` ` `
127145
128- 3. **Use the unified cache API** — inject the ` FireflyCache` (or higher-level cache facade) provided by
129- `fireflyframework-cache-core`; it is now backed by PostgreSQL with no other code changes :
146+ 3. **Use the unified cache API** — obtain a ` CacheAdapter` from the core `FireflyCacheManager`; it is
147+ now backed by PostgreSQL with no other code changes :
130148
131149` ` ` java
132- import org.fireflyframework.cache.FireflyCache;
150+ import org.fireflyframework.cache.core.CacheAdapter;
151+ import org.fireflyframework.cache.manager.FireflyCacheManager;
133152import org.springframework.stereotype.Service;
134153import reactor.core.publisher.Mono;
135154
136155@Service
137156public class ProductService {
138157
139- private final FireflyCache cache;
158+ private final CacheAdapter cache;
140159
141- public ProductService(FireflyCache cache ) {
142- this.cache = cache ; // CacheType.POSTGRES when this adapter is selected
160+ public ProductService(FireflyCacheManager cacheManager ) {
161+ this.cache = cacheManager.getCache("products") ; // CacheType.POSTGRES when selected
143162 }
144163
145164 public Mono<Product> findById(String id) {
146- return cache.get(id, Product.class)
147- .switchIfEmpty(loadFromDatabase(id)
148- .flatMap(product -> cache.put(id, product ).thenReturn(product )));
165+ return cache.<String, Product> get(id, Product.class)
166+ .flatMap(opt -> opt.map(Mono::just).orElseGet(() ->
167+ loadFromDatabase(id) .flatMap(p -> cache.put(id, p ).thenReturn(p) )));
149168 }
150169}
151170` ` `
152171
153- Because every adapter implements the same `FireflyCache ` SPI, switching between Caffeine, Redis,
154- Hazelcast, JCache and PostgreSQL is a configuration change (`firefly.cache.provider `) plus the matching
155- dependency — application code stays the same.
172+ Because every adapter implements the same `CacheAdapter ` SPI, switching between Caffeine, Redis,
173+ Hazelcast, JCache and PostgreSQL is a configuration change (`firefly.cache.default-cache-type `) plus
174+ the matching dependency — application code stays the same.
156175
157176# # Configuration
158177
159- All adapter-specific properties live under the `firefly.cache.postgres` prefix and are bound by
160- ` PostgresCacheProperties` . The provider is selected by the core-owned `firefly.cache.provider` key.
178+ All adapter-specific properties live under the `firefly.cache.postgres` prefix and are bound by the
179+ core `CacheProperties.PostgresConfig`. The active provider is chosen by the core-owned
180+ ` firefly.cache.default-cache-type` key.
161181
162182` ` ` yaml
163183firefly:
164184 cache:
165- provider: postgres # core: choose the active provider (default: caffeine )
185+ default-cache-type: POSTGRES # core: choose the active provider (default: CAFFEINE )
166186 postgres:
167- enabled: true # default: true
168- url: # R2DBC URL, e.g. r2dbc:postgresql://localhost:5432/mydb
169- username: # database username
170- password: # database password
171- table-name: firefly_cache # default: firefly_cache
172- default-ttl: 0s # default: 0 (no expiration)
173- cleanup-interval: 5m # default: 5 minutes
187+ enabled: true # default: false
188+ host: localhost # default: localhost
189+ port: 5432 # default: 5432
190+ database: appdb # default: none
191+ username: app # default: none
192+ password: secret # default: none
193+ schema: public # default: public
194+ cache-table: firefly_cache_entries # default: firefly_cache_entries
195+ cache-name: default # default: default
196+ key-prefix: "" # default: empty
197+ default-ttl: PT30M # default: 30 minutes
198+ auto-create-schema: true # default: true
199+ max-pool-size: 10 # default: 10
200+ min-pool-size: 1 # default: 1
201+ properties: {} # extra R2DBC driver options
174202` ` `
175203
176204| Property | Type | Default | Description |
177205| --- | --- | --- | --- |
178- | `firefly.cache.provider` | `CacheType` | `caffeine` | Core property that selects the active provider. Set to `postgres` to use this adapter. |
179- | `firefly.cache.postgres.enabled` | `boolean` | `true` | Enables/disables the PostgreSQL cache auto-configuration. |
180- | `firefly.cache.postgres.url` | `String` | — | R2DBC connection URL (e.g. `r2dbc:postgresql://host:5432/db`). |
206+ | `firefly.cache.default-cache-type` | `CacheType` | `CAFFEINE` | Core property selecting the active provider. Set to `POSTGRES` (or `AUTO`) to use this adapter. |
207+ | `firefly.cache.postgres.enabled` | `boolean` | `false` | Enables the PostgreSQL cache provider and its auto-configuration. |
208+ | `firefly.cache.postgres.host` | `String` | `localhost` | PostgreSQL host. |
209+ | `firefly.cache.postgres.port` | `int` | `5432` | PostgreSQL port. |
210+ | `firefly.cache.postgres.database` | `String` | — | Database name. |
181211| `firefly.cache.postgres.username` | `String` | — | Database username. |
182212| `firefly.cache.postgres.password` | `String` | — | Database password. |
183- | `firefly.cache.postgres.table-name` | `String` | `firefly_cache` | Table that stores cache entries. |
184- | `firefly.cache.postgres.default-ttl` | `Duration` | `0s` (none) | Default time-to-live for entries; zero or negative means no expiration. |
185- | `firefly.cache.postgres.cleanup-interval` | `Duration` | `5m` | How often the background job purges expired entries. |
213+ | `firefly.cache.postgres.schema` | `String` | `public` | Schema that holds the cache table. |
214+ | `firefly.cache.postgres.cache-table` | `String` | `firefly_cache_entries` | Cache table name. |
215+ | `firefly.cache.postgres.cache-name` | `String` | `default` | Default logical cache name. |
216+ | `firefly.cache.postgres.key-prefix` | `String` | `""` | Prefix applied to namespaced keys. |
217+ | `firefly.cache.postgres.default-ttl` | `Duration` | `PT30M` | Default entry TTL; zero/negative means no expiration. |
218+ | `firefly.cache.postgres.auto-create-schema` | `boolean` | `true` | Create the cache table and expiry index on startup. |
219+ | `firefly.cache.postgres.max-pool-size` | `int` | `10` | Maximum R2DBC pool connections. |
220+ | `firefly.cache.postgres.min-pool-size` | `int` | `1` | Initial/minimum R2DBC pool connections. |
221+ | `firefly.cache.postgres.properties` | `Map` | `{}` | Extra options passed to the PostgreSQL R2DBC driver. |
222+
223+ > Connection settings come **only** from `firefly.cache.postgres.*`, never from `spring.r2dbc.*` —
224+ > the cache uses a dedicated, pooled `ConnectionFactory` bean
225+ > (`fireflyCachePostgresConnectionFactory`) independent of your application datasource.
226+
227+ # ## Storage schema
228+
229+ When `auto-create-schema` is `true`, the adapter provisions (idempotently) the following table and a
230+ partial index supporting expiry scans :
231+
232+ ` ` ` sql
233+ CREATE TABLE IF NOT EXISTS "public"."firefly_cache_entries" (
234+ cache_name VARCHAR(255) NOT NULL,
235+ cache_key VARCHAR(1024) NOT NULL,
236+ value BYTEA NOT NULL,
237+ expires_at TIMESTAMPTZ,
238+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
239+ PRIMARY KEY (cache_name, cache_key)
240+ );
241+ CREATE INDEX IF NOT EXISTS firefly_cache_entries_expires_idx
242+ ON "public"."firefly_cache_entries" (expires_at) WHERE expires_at IS NOT NULL;
243+ ` ` `
244+
245+ If you disable `auto-create-schema`, create this table and index manually before first use.
186246
187247# # How It Works
188248
189- - **`PostgresCacheAutoConfiguration`** — Spring Boot `@AutoConfiguration` registered in
249+ - **`PostgresCacheAutoConfiguration`** (`org.fireflyframework.cache.config`) — Spring Boot
250+ ` @AutoConfiguration` registered in
190251 ` META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` . Activates when
191- ` CacheProvider` is on the classpath and `firefly.cache.postgres.enabled` is `true` (the default),
192- and enables `PostgresCacheProperties`.
193- - **`PostgresCacheConfiguration`** — wires the resolved `PostgresCacheProperties` into the provider.
194- - **`PostgresFireflyCache`** — the `FireflyCache` implementation. Stores entries in a relational table
195- via an R2DBC `ConnectionFactory`, serializes values with Jackson, reports `CacheType.POSTGRES`, and
196- supports TTL-based expiration with periodic cleanup of stale rows.
197- - **`PostgresCacheManager`** — manages the lifecycle of PostgreSQL-backed cache instances.
252+ the R2DBC SPI is on the classpath and `firefly.cache.postgres.enabled=true`. Builds the pooled
253+ ` fireflyCachePostgresConnectionFactory` bean and, when enabled, provisions the schema.
254+ - **`PostgresProvider`** (`org.fireflyframework.cache.spi.providers`) — the `CacheProviderFactory` SPI
255+ implementation, registered for `ServiceLoader` in
256+ ` META-INF/services/org.fireflyframework.cache.spi.CacheProviderFactory` . Reports `CacheType.POSTGRES`
257+ with priority 15 and is available when the connection factory is present and the provider is enabled.
258+ - **`PostgresCacheHelper`** (`org.fireflyframework.cache.factory`) — the factory helper the cache core
259+ references reflectively to build adapters; its fully-qualified name is load-bearing and must not be
260+ renamed or moved.
261+ - **`PostgresCacheAdapter`** (`org.fireflyframework.cache.adapter.postgres`) — the `CacheAdapter`
262+ implementation. Stores entries in the relational table via the R2DBC `ConnectionFactory`, serializes
263+ values with `JsonCacheSerializer`, namespaces keys per cache, enforces TTL, and reports
264+ ` CacheStats` /`CacheHealth`.
265+ - **`PostgresCacheConfig`** (`org.fireflyframework.cache.adapter.postgres`) — immutable per-adapter
266+ config (cache name, key prefix, default TTL, schema, table).
198267
199268# # Documentation
200269
201270- Firefly Framework documentation hub and module catalog : <https://github.com/fireflyframework>
202- - Cache abstraction SPI : [`fireflyframework-cache-core `](https://github.com/fireflyframework/fireflyframework-cache-core )
271+ - Cache abstraction & SPI : [`fireflyframework-cache`](https://github.com/fireflyframework/fireflyframework-cache)
203272- Sibling adapters :
204273 [Redis](https://github.com/fireflyframework/fireflyframework-cache-redis) ·
205274 [Hazelcast](https://github.com/fireflyframework/fireflyframework-cache-hazelcast) ·
0 commit comments