English | Türkçe
A production-oriented Spring Boot REST API that demonstrates CacheDB with Redis 8 and PostgreSQL. The sample is intentionally explicit: operational routes use a bounded Redis active data set, durable history stays in PostgreSQL, and growing lists use projections instead of full aggregates.
This release consumes the immutable CacheDB
0.12.0package from the public CacheDB Maven repository. It requires no GitHub token and does not require a local CacheDB source checkout.
After the quick start, you will have proved one complete production-shaped route: seed durable PostgreSQL rows, read the bounded archive route, warm the matching Redis projection, read the active route, and inspect coverage and durability signals. The sample does not hide a PostgreSQL query behind a Redis miss.
| Goal | Go to |
|---|---|
| Run the complete sample | Quick Start |
| Understand Redis vs PostgreSQL behavior | Runtime Contract |
| See the declarative Java API | Code Walkthrough |
| Warm existing PostgreSQL rows | Warm Existing Data |
| Choose cache limits | Tuning by Use Case |
| Exercise every route | API Catalog or Postman |
| Prove production readiness | Production Certification |
| Prepare a production rollout | Production Checklist |
| Fix a startup or data-path problem | Troubleshooting |
The domain is larger than a toy CRUD application:
- customers place orders with many order lines
- product availability serves catalog and low-stock screens
- shipments expose active, exception, event, and archive routes
- support tickets feed an operational dashboard
- report jobs and audit events separate live work from durable history
The important product boundary is equally explicit:
| Classification | Meaning |
|---|---|
| BEST | Define a bounded operational route, warm its entity or projection data, measure it, and keep archive/history reads on PostgreSQL. |
| ACCEPTABLE | Use an explicit bounded PostgreSQL route for infrequent data outside the Redis active set. |
| ANTI-PATTERN | Treat CacheDB as a transparent cache and expect every Redis miss to run an arbitrary SQL query and refill Redis automatically. |
CacheDB is a strong fit when the team knows which screens and commands require predictable low latency. It is not a fit for applications whose main workload is unbounded ad-hoc querying over the complete database.
flowchart LR
Client["REST client"] --> API["Controller"]
API --> Service["Application service"]
Service --> Repo["Generated CacheDB repository"]
Repo -->|"HotRoute / CacheLookup"| Redis[(Redis 8 active data set)]
Repo -->|"SourceRoute"| PostgreSQL[(PostgreSQL durable history)]
Repo -->|"Command"| Stream["Redis Stream write-behind"]
Stream --> Worker["Bounded persistence worker"]
Worker --> PostgreSQL
PostgreSQL -->|"WarmRoute"| Warm["Warm / backfill job"]
Warm --> Redis
The application code depends on repository interfaces. The annotation processor generates implementations, codecs, indexes, projection bindings, and Spring beans at compile time. Runtime reflection is not used for entity discovery.
| Term | Meaning in this sample |
|---|---|
| Entity | The command/detail model mapped to SQL columns and a Redis namespace |
| Projection | A compact, screen-specific read model such as OrderSummary |
| Active data set | The bounded subset intentionally available in Redis |
| Hot route | A repository method whose contract reads the Redis active data set |
| Source route | A bounded repository method that reads PostgreSQL explicitly |
| Warm/backfill | A controlled PostgreSQL-to-Redis preparation job |
| Route coverage | Evidence that the required scope/window has been prepared in Redis |
| Write-behind | An accepted Redis command that is persisted to PostgreSQL asynchronously |
| Write receipt | The command result used to track identity, version, and durability state |
- JDK 21
- Maven 3.9+
- Docker Desktop or a compatible Docker Engine
- PowerShell 7+ for the bundled load script
Verify the local toolchain:
java -version
mvn -version
docker version
docker compose versionThe sample consumes CacheDB as Maven artifacts. It does not compile framework sources as part of the sample build.
<properties>
<java.version>21</java.version>
<cachedb.version>0.12.0</cachedb.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.reactor.cachedb</groupId>
<artifactId>cachedb-bom</artifactId>
<version>${cachedb.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.reactor.cachedb</groupId>
<artifactId>cachedb-spring-boot-starter-postgres</artifactId>
</dependency>
<dependency>
<groupId>com.reactor.cachedb</groupId>
<artifactId>cachedb-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
<annotationProcessorPaths>
<path>
<groupId>com.reactor.cachedb</groupId>
<artifactId>cachedb-processor</artifactId>
<version>${cachedb.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>Add cachedb-spring-boot-starter-admin only when the operations UI is needed.
If JPA or another starter already creates the application DataSource, adding
spring-boot-starter-jdbc again is not required. CacheDB needs one working
DataSource, one provider starter, the annotations artifact, and the annotation
processor.
Maven resolves normal dependencies and build plugins from separate repository lists, so the public CacheDB repository appears in both sections:
<repositories>
<repository>
<id>cachedb-public</id>
<url>https://esasmer-dou.github.io/cache-database/maven2</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>cachedb-public</id>
<url>https://esasmer-dou.github.io/cache-database/maven2</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</pluginRepository>
</pluginRepositories>repositories resolves the BOM, starters, and libraries.
pluginRepositories resolves cachedb-maven-plugin. The endpoint is public;
no Maven settings.xml, username, or token is required.
Validate the sample directly. Maven resolves the BOM, starter, annotation
processor, and doctor plugin anonymously from the immutable 0.12.0 package:
mvn -U -DskipTests validateThe build must print CacheDB doctor and
OK: CacheDB build contract is consistent. It must not depend on a locally
installed CacheDB checkout.
docker compose up -d
docker compose psThe compose file starts:
| Service | Address | Local purpose |
|---|---|---|
| Redis 8.2.1 | 127.0.0.1:56379 |
Active entities, projections, indexes, streams, leases, and telemetry |
| PostgreSQL 16 | 127.0.0.1:55432 |
Durable source of truth |
The demo profile is required for local schema initialization, seed endpoints,
warm endpoints, scheduled warm, and the admin UI.
Spring owns the provider-specific schema.sql in this sample. CacheDB starts
after that script and validates the result with VALIDATE_ONLY; it does not
compete with Spring for DDL ownership.
$env:SPRING_PROFILES_ACTIVE = "demo"
mvn spring-boot:runBash equivalent:
SPRING_PROFILES_ACTIVE=demo mvn spring-boot:runInvoke-RestMethod http://127.0.0.1:8091/actuator/health/readinessContinue only when the status is UP. Readiness covers Redis, PostgreSQL, and
write-behind health; liveness only proves that the process is running.
Seed runs as a bounded distributed job and returns 202 Accepted. Poll the job
instead of keeping one HTTP request open:
$seed = Invoke-RestMethod -Method Post `
-Uri "http://127.0.0.1:8091/api/demo/seed?customers=20&ordersPerCustomer=40&linesPerOrder=4"
do {
Start-Sleep -Milliseconds 250
$seedState = Invoke-RestMethod "http://127.0.0.1:8091/api/warm/jobs/$($seed.jobId)"
} while ($seedState.status -in @("QUEUED", "RUNNING"))
if ($seedState.status -ne "COMPLETED") {
throw ($seedState | ConvertTo-Json -Depth 8)
}COMPLETED means the seed job finished. Before a production cutover, also
observe readiness and the write-behind backlog until SQL durability is healthy.
Seed creates durable demo rows, but it does not declare every Redis route
complete. A hot list intentionally returns 503 Service Unavailable until its
matching warm job finishes and coverage is recorded. This prevents an empty or
partial Redis window from being mistaken for a complete business result.
$warm = Invoke-RestMethod -Method Post `
-Uri "http://127.0.0.1:8091/api/warm/orders/customer/1?limit=100&projectionOnly=true"
do {
Start-Sleep -Milliseconds 250
$warmState = Invoke-RestMethod "http://127.0.0.1:8091/api/warm/jobs/$($warm.jobId)"
} while ($warmState.status -in @("QUEUED", "RUNNING"))
if ($warmState.status -ne "COMPLETED") {
throw ($warmState | ConvertTo-Json -Depth 8)
}# Redis projection route
$page = Invoke-RestMethod "http://127.0.0.1:8091/api/customers/1/orders?limit=10"
$page.items
# Continue without offset when nextCursor is present
if ($page.nextCursor) {
Invoke-RestMethod "http://127.0.0.1:8091/api/customers/1/orders?limit=10&after=$($page.nextCursor)"
}
# Bounded PostgreSQL route
Invoke-RestMethod "http://127.0.0.1:8091/api/orders/archive?customerId=1&limit=10"- Admin UI:
http://127.0.0.1:8091/cachedb-admin - Tuning snapshot:
http://127.0.0.1:8091/api/tuning - Scheduled warm status:
http://127.0.0.1:8091/api/warm/schedules
Stop the local infrastructure without deleting PostgreSQL data:
docker compose downUse docker compose down -v only when the local PostgreSQL volume should be
deleted intentionally.
| Operation | Primary path | If data is outside Redis | Durability / safety rule |
|---|---|---|---|
save, update, soft delete |
Redis first, then PostgreSQL write-behind | The command can enter Redis when admission policy permits | 202 Accepted is not the same as SQL commit; inspect the receipt and readiness telemetry |
| Entity detail | Redis entity lookup | Returns an explicit unavailable/not-found outcome; it does not run arbitrary SQL automatically | Warm the entity route or expose a bounded source-detail route |
| Growing list or dashboard | Redis projection | completeItems() returns 503 Service Unavailable until the exact route scope is warm and complete |
Warm the matching route, poll the job to COMPLETED, and require coverage before cutover |
| Archive, export, audit history | Bounded PostgreSQL source route | Reads PostgreSQL directly | Apply row limits, deterministic ordering, indexes, and timeouts |
| Existing PostgreSQL rows | Warm/backfill reads PostgreSQL and hydrates Redis | No automatic startup import | Dry-run first, run a bounded warm job, then verify coverage |
| External PostgreSQL write | PostgreSQL changes first | Redis can become stale without a feed | Integrate outbox/CDC; periodic warm is reconciliation, not a replacement for event propagation |
The active data set is a deliberate subset, not a second complete copy of the database. Redis memory must be budgeted for entity payloads, projections, indexes, stream state, leases, and operational metadata.
OrderEntity
maps SQL columns, Redis namespace, a partitioned index, and a bounded relation:
@CacheEntity(table = "sample_orders", redisNamespace = "sample-orders")
@CachePartitionedIndex(partitionBy = "customer_id", sortBy = "order_date")
public class OrderEntity {
@CacheId(column = "order_id")
public Long orderId;
@CacheColumn("customer_id")
public Long customerId;
@CacheColumn("order_date")
public Long orderDate;
@CacheRelation(
target = OrderLineEntity.class,
mappedBy = "orderId",
kind = CacheRelation.RelationKind.ONE_TO_MANY,
batchLoadOnly = true,
maxRowsPerParent = 50,
parentBatchSize = 16,
orderBy = "lineNumber ASC"
)
public List<OrderLineEntity> lines;
}The database foreign key protects durable relational integrity.
@CacheRelation tells CacheDB how to load and bound the relation. Either can
exist without the other, but production models normally need both.
OrderSummary
is smaller than OrderEntity and excludes order-line payloads:
@CacheProjectionRecord(
source = OrderEntity.class,
id = "orderId",
name = "order-summary",
rankedBy = {"order_date", "priority_score"},
refresh = CacheProjectionRecord.Refresh.ASYNC
)
public record OrderSummary(
Long orderId,
Long customerId,
Long orderDate,
BigDecimal orderAmount,
String currencyCode,
String orderType,
String status,
Integer lineCount,
Double priorityScore
) {
}Use an entity for commands and selected detail. Use a projection for a list, timeline, dashboard, top-N, or global sorted route.
OrderRepository
declares the route; the processor generates its implementation:
@CacheRepository(entity = OrderEntity.class)
@CacheRepositoryDefaults(
hotPopulation = HotRoute.Population.DECLARED_WARM,
sourceMaxRows = 500,
sourceTimeoutSeconds = 15
)
public interface OrderRepository extends CacheDbRepository<OrderEntity, Long> {
@HotRoute(
value = "customer-order-timeline",
projection = OrderSummary.class,
pageSize = 100,
hotWindow = 1_000,
memoryBudgetBytes = CacheMemoryBudget.MIB_16,
coverageScopeParameter = "customerId"
)
@CacheRouteQuery(
predicates = @CachePredicate(field = "customerId"),
orderBy = {
@CacheOrder(field = "orderDate", direction = CacheOrder.Direction.DESC),
@CacheOrder(field = "orderId", direction = CacheOrder.Direction.DESC)
}
)
CursorPage<OrderSummary> customerTimeline(long customerId, WindowRequest window);
@WarmRoute(
value = "warm-customer-order-timeline",
from = "customerTimeline",
maxRows = 1_000
)
CacheWarmPlan warmCustomerTimeline(
long customerId,
int maxRows,
CacheWarmTarget target
);
}The route contract puts the page size, active window, memory budget, sort order,
coverage scope, and warm limit in one reviewable place. The processor infers the
same-name predicate parameter, the single WindowRequest, the warm row-limit,
warm target, and warm coverage scope. Ambiguous declarations fail compilation;
the processor never chooses between multiple candidates silently.
CustomerApplicationService
injects interfaces, not Redis clients or generated binding classes:
@Service
public final class CustomerApplicationService {
private final CustomerRepository customers;
private final OrderRepository orders;
public CustomerEntity detail(long customerId, int orderPreview) {
return SampleHotLookups.require(
"Customer",
customerId,
customers.detail(customerId, orderPreview)
);
}
public CursorPage<OrderSummary> orderTimeline(long customerId, int limit, String after) {
return orders.customerTimeline(customerId, WindowRequest.of(limit, after));
}
}Controllers validate HTTP input. Application services own use-case orchestration. Repository interfaces own data-path contracts. Generated code owns serialization, indexes, and provider wiring.
The REST endpoint accepts the returned nextCursor as the next request's
optional after parameter. The cursor is bound to this route, customer scope,
and sort contract; it cannot be reused for another customer or route. A strict
HOT route may return CursorPage<T> directly because generated code verifies
complete and fresh coverage before creating the page. Return HotWindow<T>
instead when application code must inspect coverage evidence itself.
The processor also generates a reflection-free route companion. Use it in integration tests and operational code instead of repeating route-name strings:
cacheDbTestProbe.requireDeclaredWarmRoute(
OrderRepositoryCacheDbRoutes.customerTimeline()
);
RouteCoverage coverage = cacheDbTestProbe.coverage(
OrderRepositoryCacheDbRoutes.customerTimeline(),
String.valueOf(customerId),
Duration.ofMinutes(5)
);If the repository method or route contract changes, this code changes at
compile time. The generated companion is under target/generated-sources; do
not copy it into src/main/java.
Existing PostgreSQL rows are not imported at startup. Use this sequence for an existing system:
- Define one bounded
@HotRouteor@CacheLookup. - Add the matching
@WarmRoute. - Run
dryRun=trueand inspect the candidate row count. - Submit the real warm job and poll it to
COMPLETED. - Verify route coverage and compare membership/order with PostgreSQL.
- Enable traffic gradually and retain the PostgreSQL rollback path.
Dry-run example:
Invoke-RestMethod -Method Post `
-Uri "http://127.0.0.1:8091/api/warm/orders/customer/1?limit=100&projectionOnly=true&dryRun=true"Projection-only warm is best for lists and dashboards. Entity warm is justified only when selected detail or command routes need the complete active payload.
For legacy tables, an entity_version value of NULL or 0 is normalized to
the initial Redis version during the first warm. This is a migration aid, not a
change-feed strategy. After cutover, every external PostgreSQL writer must
advance a monotonic version and publish through outbox/CDC, or the next bounded
reconciliation cycle must be an explicitly accepted lag window.
SampleScheduledWarmPlans
declares a 90-day order window. Redis leases ensure that only one pod performs a
scheduled cycle while other pods wait or skip safely. Reconciliation removes
rows that no longer satisfy the policy.
@CacheScheduledWarm(
name = "sample-active-order-window",
fixedDelayString = "${sample.scheduled-warm.orders.fixed-delay:PT15M}",
lockAtMostForString = "${sample.scheduled-warm.orders.lock-at-most-for:PT2M}",
lockWaitTimeoutString = "${sample.scheduled-warm.orders.lock-wait-timeout:PT20S}",
minimumIntervalString = "${sample.scheduled-warm.orders.minimum-interval:PT15M}",
reconcileHotSet = true
)
public CacheWarmPlan activeOrderWindow() {
long cutoff = Instant.now().minus(Duration.ofDays(90)).getEpochSecond();
return orders.warmActiveWindow(cutoff, orderWarmMaxRows);
}Scheduled warm maintains the chosen active window. New CacheDB writes still enter through the normal command path immediately; they do not wait for the next schedule.
The annotation processor validates the method and generates a typed Spring task adapter. The runtime does not scan annotated methods or invoke them through reflection.
A typed target selects the payload once. The generated plan then owns that decision, while execution selects only dry-run or apply:
CacheWarmTarget target = projectionOnly
? CacheWarmTarget.PROJECTIONS_ONLY
: CacheWarmTarget.ENTITY_AND_PROJECTIONS;
CacheWarmPlan plan = orders.warmCustomerTimeline(customerId, limit, target);
CacheWarmExecution execution = cacheDatabase.executeWarm(
plan,
dryRun ? CacheWarmExecutionMode.DRY_RUN : CacheWarmExecutionMode.APPLY
);
CacheWarmSummary summary = execution.summary("customer-orders");Dry-run never mutates Redis. Do not create separate entity/projection methods
for the same route or repeat the decision with a second
warmProjections/warm branch.
The REST endpoint does not execute a backfill on the request thread. It submits
one typed command to the durable Redis job lane and returns 202 Accepted with
a Location header:
public record SampleWarmCommand(Route route, int limit, boolean projectionOnly,
boolean dryRun) {
}
static final CacheDistributedJobDefinition<SampleWarmCommand> WARM_JOB =
CacheDistributedJobDefinition.of("sample.route.warm", SampleWarmCommand.class);
CacheDistributedJobSnapshot job = jobs.submit(WARM_JOB, command);
// Location: /api/warm/jobs/{jobId}The handler implements CacheDistributedJobHandler.Typed<SampleWarmCommand>
and returns this same definition. It stores bounded progress with
CacheDistributedJobProgress; route strings and payload types are not repeated.
Every pod registers the same job definition. Redis stores status and checkpoints, so another pod can claim abandoned work. The route still keeps bounded row limits, idempotent warm behavior, and generated SQL; the job layer does not turn the operation into an unbounded background query.
Predicate groups are also explicit. Predicates in one group are ANDed; separate
groups are ORed. The active-order window deliberately means "last 90 days OR
an active state", so its @CacheRouteQuery uses groups 0 and 1 together
with explicitDisjunction = true. A multi-group query without that flag fails
compilation. Keep predicates in one group when the business rule is AND.
Seed and import batches preserve SQL durability evidence:
try (var orders = cacheDatabase.durableBatchWriter(
"sample seed/orders", 128, 1_024, Duration.ofSeconds(30),
orderRepository::saveAll
)) {
sourceOrders.forEach(orders::add);
}A timeout means Redis accepted the command but SQL durability was not proven within the deadline. The exception retains the receipts and operation name; do not issue blind duplicate writes.
The processor generates a route catalog for every repository, and the starter aggregates it without runtime reflection:
Invoke-RestMethod http://127.0.0.1:8091/actuator/cachedbThe response contains declared route counts/kinds, HOT-route population
strategies, at most 250 route details, at most 100 scheduled-warm details, and
truncation flags. Aggregate Micrometer meters include
cachedb.repositories.declared, cachedb.routes.declared,
cachedb.routes.hot.population{strategy=...}, and the
cachedb.scheduled.warm.* meters. The strategy tag has four bounded values;
route, customer, and tenant names are never metric tags. The catalog proves
what was compiled; route coverage, parity, latency, memory, and SQL durability
remain separate production gates.
Start from measured route demand, not from a guess about table size.
| Scenario | Active data policy | Read model | Initial limits | Outside the active set |
|---|---|---|---|---|
| Customer order timeline | Last 90 days or active order states | OrderSummary per customer |
Page 100, window 1,000, route budget 16 MiB |
Bounded archive source route |
| Product catalog | Active products or in-stock/low-stock states | ProductAvailability |
Entity limit 25,000, page 100 |
Inactive-product source route |
| Support operations | Updated in 30 days or OPEN/PENDING/ESCALATED |
Entity for small rows | Entity limit 50,000, page 50 |
Explicit ticket-history SQL route when needed |
| Logistics control tower | Updated in 14 days or active/exception states | ShipmentSummary |
Entity limit 150,000, route windows 2,000-10,000 |
Delivered-shipment source route |
| Report execution | QUEUED/RUNNING/FAILED or last 24 hours |
Small job entity | Entity limit 5,000, page 50 |
Completed report history on PostgreSQL |
| Security audit | Severe events from the last 24 hours | Small bounded entity list | Entity limit 2,000, read admission disabled |
Full audit archive on PostgreSQL |
The sample exposes the resolved runtime configuration:
Invoke-RestMethod http://127.0.0.1:8091/api/tuning
Invoke-RestMethod http://127.0.0.1:8091/api/tuning/profilesKey controls in
SampleCacheDbTuningConfig:
| Control | Sample value | Why it exists |
|---|---|---|
maxEntityQueryLimit |
250 |
Stops broad entity materialization |
maxProjectionQueryLimit |
1,000 |
Allows larger compact read-model windows |
maxQueryLoadRows |
1,000 |
Bounds registered source loading |
queryTimeoutSeconds |
15 |
Bounds source reads |
workerThreads |
2 |
Limits concurrent SQL flush pressure |
batchSize / maxFlushBatchSize |
128 |
Reduces SQL round trips without unbounded batches |
| Redis warning / critical thresholds | 75% / 88% |
Adds backpressure before Redis reaches maxmemory |
| expected eviction policy | noeviction |
Prevents Redis from silently dropping coordination or write state |
Do not copy these numbers directly to production. Measure serialized payload size, projection/index overhead, peak route concurrency, SQL flush latency, and the Redis memory headroom needed during rewarm or failover.
| Area | Representative endpoints | Data path |
|---|---|---|
| Health and operations | GET /actuator/health/readiness, GET /api/tuning, GET /api/warm/schedules |
Runtime telemetry |
| Demo setup | POST /api/demo/seed, GET /api/warm/jobs/{jobId} |
Distributed background jobs |
| Customers | POST /api/customers, GET /api/customers/{id}, GET /api/customers/{id}/orders |
Command, entity detail, projection timeline |
| Orders | POST /api/orders, PATCH /api/orders/{id}/status, DELETE /api/orders/{id} |
Redis-first write-behind commands |
| Order reads | GET /api/orders/{id}, GET /api/orders/high-value, GET /api/orders/archive |
Entity, ranked projection, PostgreSQL source route |
| Products | GET /api/products/active, GET /api/products/low-stock, PATCH /api/products/{id}/stock |
Projection reads and command |
| Shipments | GET /api/shipments/active, GET /api/shipments/exceptions, GET /api/shipments/archive |
Projection and PostgreSQL source routes |
| Support | GET /api/tickets/open, POST /api/tickets, PATCH /api/tickets/{id}/status |
Bounded entity read and commands |
| Reporting | GET /api/reports/jobs/live, GET /api/reports/audit/security, GET /api/reports/audit/archive |
Active rows and durable archive |
| Dashboards | GET /api/dashboard/commerce, GET /api/dashboard/operations |
Pre-shaped Redis data |
| Warm | POST /api/warm/customers/active, /orders/customer/{id}, /orders/{id}/lines, /orders/high-value, /orders/highlighted, /products/active, /products/low-stock, /tickets/open, /shipments/active, /shipments/customer/{id}, /shipments/exceptions, /shipments/{id}/events, /reports/live, /reports/type/{type}, /audit/security |
Bounded PostgreSQL-to-Redis jobs; poll /api/warm/jobs/{jobId} after every submission |
Request limits are validated. Oversized values return 400 Bad Request rather
than being silently clamped. Queue saturation returns 429 Too Many Requests.
Optimistic conflicts and not-yet-durable parent references return 409 Conflict.
Import:
postman/cache-database-postgresql-sample.postman_collection.json
Run folders in this order:
- Run readiness, submit demo seed, and repeat
Latest Background Job Statusuntil it reportsCOMPLETED. - In each business folder, run the matching
Warm ...request before its hot list request. - After every
202 Accepted, repeatLatest Background Job Statusuntil the submitted job reportsCOMPLETED. - Call the hot route and compare it with the bounded archive/source route where the folder provides one.
- Run the dashboard folder only after its component routes have been warmed.
- Inspect tuning and scheduled-warm status before changing any window or pool.
After seed and warm complete:
powershell -ExecutionPolicy Bypass -File .\scripts\run-load-test.ps1 `
-RouteProfile hot-timeline `
-Concurrency 8 `
-DurationSeconds 20 `
-SeedCustomers 20 `
-OrdersPerCustomer 40 `
-WarmCustomers 20 `
-WarmLimit 100 `
-MaxP95Millis 250This is a local regression gate, not a production capacity result. Production numbers require realistic latency to Redis/PostgreSQL, Kubernetes resource limits, representative payloads, and expected concurrency.
- Index every source-route predicate and deterministic sort suffix.
- Keep HikariCP below the PostgreSQL connection budget across all pods.
- Tune write-behind batch size against WAL pressure and lock duration.
- Use statement timeouts for warm, archive, and migration queries.
- Integrate outbox/CDC when another application can write the same tables.
- Prove backup, restore, Redis loss/rebuild, and application rollback paths.
- Treat PostgreSQL as the durable source of truth even when Redis serves the operational route.
The framework repository proves CacheDB's own Docker and provider behavior.
Your application must separately prove its routes and its real staging
topology. After collecting route coverage, parity, memory, failover, canary,
and rollback evidence under cachedb-certification/, run:
mvn verify -Pproduction-certificationThe command fails on missing routes, evidence from another commit or
environment, unresolved parity, an exceeded memory budget, or a missing
failover/rollback drill. It writes the shareable result to
target/cachedb-production-certification.md. Use the complete
production certification contract;
never copy placeholder evidence and mark it as passed.
- Every operational endpoint is classified as command, active entity, projection, or source route.
- Every active route has a page limit, active window, memory budget, sort order, and coverage scope.
- Relation-heavy and globally sorted screens use projections.
- Warm/backfill is bounded, resumable, observable, and tested after Redis loss.
- Source routes have matching indexes, timeouts, and maximum row counts.
-
202 Accepteddurability semantics are understood by callers. - Redis uses explicit
maxmemory,noeviction, alerting, and capacity headroom. - PostgreSQL and HikariCP connection budgets are calculated per pod and for the full replica count.
- Multi-pod scheduled warm and abandoned-job claiming are tested.
- External database writes are covered by outbox/CDC or an explicit reconciliation decision.
- Admin endpoints are disabled or protected behind the internal gateway.
- Side-by-side parity, latency, canary, rollback, and recovery evidence is recorded before cutover.
| Symptom | Likely cause | Fix |
|---|---|---|
/api/demo/seed or /api/warm/** returns 404 |
Application was not started with the demo profile |
Set SPRING_PROFILES_ACTIVE=demo and restart |
CacheDB artifacts return 404 |
The requested version is not published or the public repository URL is missing | Use a released stable version and declare https://esasmer-dou.github.io/cache-database/maven2 in both repository sections |
Dependencies resolve but cachedb-maven-plugin does not |
pluginRepositories is absent |
Add the same public CacheDB URL under pluginRepositories |
production-certification fails |
Route evidence is missing, stale, or belongs to another commit/environment | Read the generated report, regenerate real staging evidence, and rerun the profile |
Active route returns 503 while archive returns rows |
Redis route has not been warmed, its coverage expired, or the scope differs | Run dry-run, warm the exact route/scope, poll to COMPLETED, then inspect coverage |
| Detail route reports unavailable data | Entity payload is outside the active set | Warm entities for that detail scope or add a bounded source-detail route |
409 Conflict after a parent write |
Parent is not durable yet or optimistic version changed | Honor Retry-After, verify write-behind health, retry idempotently |
429 Too Many Requests |
Bounded job queue or backpressure guard is active | Reduce producer rate and inspect Redis/write-behind telemetry |
Readiness is DOWN |
Redis, PostgreSQL, dead-letter, recovery, or backlog condition failed | Inspect readiness details and logs; do not route traffic yet |
| Redis reaches memory warning | Active set, projection/index overhead, or backlog exceeded budget | Stop admission growth, measure keyspace, reduce windows, or add capacity |