https://github.com/hunghhdev/pgcache
Simple and reliable PostgreSQL-backed caching solution for Spring-based applications
https://github.com/hunghhdev/pgcache
cache java postgresql spring
Last synced: about 1 month ago
JSON representation
Simple and reliable PostgreSQL-backed caching solution for Spring-based applications
- Host: GitHub
- URL: https://github.com/hunghhdev/pgcache
- Owner: hunghhdev
- License: mit
- Created: 2025-07-09T18:00:58.000Z (12 months ago)
- Default Branch: master
- Last Pushed: 2026-05-12T01:11:01.000Z (about 1 month ago)
- Last Synced: 2026-05-12T03:13:01.503Z (about 1 month ago)
- Topics: cache, java, postgresql, spring
- Language: Java
- Homepage:
- Size: 313 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# PgCache
[](https://www.oracle.com/java/)
[](https://central.sonatype.com/artifact/io.github.hunghhdev/pgcache)
[](LICENSE)
**A simple caching library that uses your existing PostgreSQL as cache backend.**
Perfect for small-to-medium applications that want caching without the complexity and cost of dedicated cache infrastructure.
## Why PgCache?
**You already have PostgreSQL. Why add Redis?**
| Scenario | Traditional Approach | With PgCache |
|----------|---------------------|--------------|
| Small/Medium app | PostgreSQL + Redis | PostgreSQL only |
| Infrastructure | 2 systems to maintain | 1 system |
| Monthly cost | -200+ for Redis | /usr/bin/zsh extra |
| Complexity | Connection pools, failover for both | Single database |
### Best For
- Startups and small teams wanting to keep infrastructure simple
- Applications with moderate caching needs (< 100k cache entries)
- Projects where PostgreSQL is already the primary database
- When you need caching but Redis/Memcached is overkill
### Not Ideal For
- High-throughput systems needing millions of ops/sec
- Sub-millisecond latency requirements
- Systems already using Redis with complex data structures
- Very large cache datasets (> 1M entries)
## Features
- **Zero extra infrastructure** - Uses your existing PostgreSQL
- **Spring Boot integration** - Works with `@Cacheable`, `@CacheEvict`, auto-configuration
- **Quarkus integration** - Works with `@CacheResult`, MicroProfile Health
- **Async API** - Non-blocking operations (`getAsync`, `putAsync`)
- **Event Listeners** - Monitor cache events (put, evict, clear)
- **Sliding TTL** - Active entries stay cached longer (like Redis)
- **Null value caching** - Properly cache null results
- **Batch operations** - `getAll`, `putAll`, `evictAll` for efficiency
- **Cache statistics** - Hit/miss counts, hit rate monitoring
- **Micrometer metrics** - Auto-configured metrics for monitoring
- **Pattern eviction** - Evict keys by pattern (e.g., `user:%`)
- **JSONB storage** - Efficient storage with PostgreSQL's native JSON
- **UNLOGGED tables** - Optimized for cache performance
- **Background cleanup** - Automatic expired entry removal
## Quick Start
### Maven
```xml
io.github.hunghhdev
pgcache-core
1.7.0
io.github.hunghhdev
pgcache-spring
1.7.0
io.github.hunghhdev
pgcache-quarkus
1.7.0
```
### Spring Boot Usage
```java
@SpringBootApplication
@EnablePgCache
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Service
public class UserService {
@Cacheable("users")
public User getUser(Long id) {
return userRepository.findById(id);
}
@CacheEvict("users")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
```
### Configuration
```yaml
# application.yml
pgcache:
default-ttl: PT1H # 1 hour default TTL
allow-null-values: true
background-cleanup:
enabled: true
interval: PT30M # Cleanup every 30 minutes
caches:
users:
ttl: PT2H
ttl-policy: SLIDING # Reset TTL on access
products:
ttl: PT6H
ttl-policy: ABSOLUTE # Fixed expiration
```
### Quarkus Usage
```java
@ApplicationScoped
public class UserService {
@Inject
PgQuarkusCacheManager cacheManager;
public Uni getUser(Long id) {
PgQuarkusCache cache = (PgQuarkusCache) cacheManager.getCache("users").get();
// Uses async API under the hood
return cache.getAsync("user:" + id, key -> userRepository.findById(id));
}
}
```
**Quarkus Configuration:**
```properties
# application.properties
pgcache.default-ttl=PT1H
pgcache.allow-null-values=true
pgcache.ttl-policy=ABSOLUTE
pgcache.background-cleanup.enabled=true
pgcache.background-cleanup.interval=PT30M
# Per-cache settings
pgcache.caches.users.ttl=PT2H
pgcache.caches.users.ttl-policy=SLIDING
```
### Standalone Usage (Core)
```java
PgCacheClient cache = PgCacheStore.builder()
.dataSource(dataSource)
.build();
// Store with TTL
cache.put("user:123", user, Duration.ofHours(1));
// Retrieve
Optional user = cache.get("user:123", User.class);
// Async Operations (v1.6.2)
cache.getAsync("user:123", User.class)
.thenAccept(opt -> System.out.println(opt.orElse(null)));
// Key Check (v1.6.2) - Efficient existence check
boolean exists = cache.containsKey("user:123");
// Pattern Operations
cache.getKeys("user:%"); // Get all user keys
cache.evictByPattern("user:%"); // Evict all user keys
```
### Event Listeners (v1.6.2)
Monitor cache operations by registering listeners:
```java
PgCacheStore.builder()
.dataSource(dataSource)
.addEventListener(new CacheEventListener() {
@Override
public void onPut(String key, Object value) {
log.info("Cached: {}", key);
}
@Override
public void onEvict(String key) {
log.info("Evicted: {}", key);
}
})
.build();
```
In **Spring Boot**, simply define a bean implementing `CacheEventListener`:
```java
@Component
public class MyCacheListener implements CacheEventListener {
// ... overrides
}
```
### Quarkus Health Check (v1.6.2)
Add cache health status to your Quarkus Health endpoint:
```java
@Liveness
@ApplicationScoped
public class CacheHealthCheck implements HealthCheck {
@Inject PgQuarkusHealthCheck pgCacheHealth;
@Override
public HealthCheckResponse call() {
return pgCacheHealth.check().isUp()
? HealthCheckResponse.up("pgcache")
: HealthCheckResponse.down("pgcache");
}
}
```
## Advanced Features
### Batch Operations (v1.3.0)
```java
// Get multiple values at once
Map users = cache.getAll(
Arrays.asList("user:1", "user:2", "user:3"),
User.class
);
// Put multiple values
Map entries = Map.of(
"user:1", user1,
"user:2", user2
);
cache.putAll(entries, Duration.ofHours(1));
```
### Cache Statistics
```java
CacheStatistics stats = cache.getStatistics();
System.out.println("Hit Rate: " + stats.getHitRate());
```
### Micrometer Metrics
When using `pgcache-spring` with Micrometer, metrics are auto-configured:
- `pgcache.gets` (counter)
- `pgcache.puts` (counter)
- `pgcache.evictions` (counter)
- `pgcache.size` (gauge)
- `pgcache.hit.rate` (gauge)
## Sliding vs Absolute TTL
```java
// Absolute TTL: expires at creation_time + TTL
cache.put("key", value, Duration.ofHours(1));
// Sliding TTL: expires at last_access_time + TTL
cache.put("key", value, Duration.ofHours(1), TTLPolicy.SLIDING);
```
## Performance
PgCache uses **UNLOGGED tables** (no WAL overhead) and **JSONB** storage for performance.
- **Read**: ~1-5ms
- **Write**: ~2-10ms
- **Throughput**: Hundreds to low thousands ops/sec
## Migration
### From 1.6.x to 1.7.0
No database changes required. Backward-compatible release with bug fixes and DRY refactor.
**Bug fixes:**
- Cache names containing `_` or `%` no longer cause cross-cache eviction (SQL `LIKE` wildcards now escaped).
- Concurrent `setCacheConfiguration` / `removeCache` / `getCache` no longer racy.
- `PgCacheManager.removeCache` returns `true` even when post-removal `clear()` fails.
**New API:**
- `PgCacheClient.size(String pattern)` — efficient `COUNT(*)` size lookup.
- `TTLPolicy.parse(String)` / `parseOrDefault(String)`.
- `NullValueMarker.MARKER_VALUE`, `NullValueMarker.isMarker(Object)`.
- `SqlPatterns.escapeLikePattern(String)`.
- `PgCacheManager.getStoreStatistics()` — per-store metrics for health endpoints.
**Deprecations (removed in 2.0.0):**
- `PgCacheStore(DataSource[, boolean])` ctors → use `PgCacheStore.builder()`.
- `PgCacheManager.PgCacheConfiguration` → use `CacheStoreConfig`.
- `PgCache.cleanupExpired()` / `PgQuarkusCache.cleanupExpired()` → use `cleanupExpiredAllCaches()`.
- `PgQuarkusCacheManager.getOrCreateCache(String)` → use `(PgQuarkusCache) getCache(name).get()`.
- `PgQuarkusCacheManager.CacheConfig`: `isAllowNullValues()` and 3-arg ctor removed; setter changed to `setAllowNullValues(Boolean)` for 3-state semantics.
### From 1.5.x to 1.6.0
No database changes required.
**New Features:**
- **Async API**: `getAsync`, `putAsync` in `PgCacheClient`
- **Key Operations**: `containsKey`, `getKeys(pattern)`
- **Event Listeners**: `CacheEventListener` interface
- **Quarkus**: `PgQuarkusCache` now uses non-blocking async internal API
- **Spring**: Auto-detection of `CacheEventListener` beans
### From 1.4.x to 1.5.x
No database changes required. Added Quarkus integration.
### From 1.3.x to 1.4.0
No database changes required. Added Micrometer metrics.
### From 1.2.x to 1.3.0
No database changes required. Added Batch operations and Statistics.
### From 1.0.x/1.1.x to 1.2.x
Schema update required for Sliding TTL:
```sql
ALTER TABLE pgcache_store
ADD COLUMN IF NOT EXISTS ttl_policy VARCHAR(10) DEFAULT 'ABSOLUTE',
ADD COLUMN IF NOT EXISTS last_accessed TIMESTAMP DEFAULT now();
CREATE INDEX IF NOT EXISTS pgcache_store_sliding_ttl_idx
ON pgcache_store (ttl_policy, last_accessed)
WHERE ttl_policy = 'SLIDING';
```
## License
MIT License - see [LICENSE](LICENSE) for details.