1354 lines
65 KiB
Markdown
1354 lines
65 KiB
Markdown
# Production Capability Platform Design
|
||
|
||
- Date: 2026-07-26
|
||
- Status: Proposed for implementation approval
|
||
- Scope: architecture and staged implementation design only
|
||
- Baseline: Java 21, Spring Boot 4.0.0, Gradle multi-module Clean Architecture template
|
||
|
||
## 1. Executive decision
|
||
|
||
This repository should evolve from a collection of integration seams into an **opt-in production
|
||
capability platform**.
|
||
|
||
The target is not to enable Redis, Kafka, MongoDB, sessions, CDC, and every transport in every
|
||
application. The target is:
|
||
|
||
1. a developer selects a capability and provider by typed configuration;
|
||
2. the composition root validates the selected topology and guarantees at startup;
|
||
3. application code depends only on semantic, framework-free ports;
|
||
4. the selected adapter supplies a real client, bounded defaults, health, metrics, failure
|
||
semantics, and reusable contract tests;
|
||
5. unused capabilities create no connection, background worker, schema, or implicit runtime
|
||
behavior;
|
||
6. advanced strategies remain available without pretending that one strategy is correct for every
|
||
domain.
|
||
|
||
The default template remains light. Production capability packs are **available by default but
|
||
inactive by default**. A capability is not called production-ready merely because a class or client
|
||
seam exists.
|
||
|
||
### Primary decisions
|
||
|
||
- Keep `domain-core`, `application-core`, and `shared-contract` free of Spring, Redis, Kafka,
|
||
persistence, transport, and observability SDK types.
|
||
- Put use-case-owned semantic ports and application policies in `application-core`; put
|
||
skeleton-wide transport/operational contracts in `shared-contract`; put provider selection and
|
||
Spring composition in `app-bootstrap`.
|
||
- Initially preserve the registered 19-leaf topology. Expand the existing technology leaves in
|
||
cohesive packages and split a leaf only when inbound/outbound direction or independent lifecycle
|
||
requires it.
|
||
- Treat `adapter:outbound:cache-redis` as the first Redis technology capability provider, but never
|
||
reuse cache's fail-open behavior for sessions, idempotency, locks, or strict rate limits.
|
||
- Add a future `adapter:inbound:messaging-kafka` leaf before implementing Kafka consumers. A
|
||
consumer is a driving adapter and does not belong in the existing outbound producer leaf.
|
||
- Keep an outbox append operation in the same source-of-truth datastore transaction as the business
|
||
write. Make the **dispatch mechanism** selectable: polling or CDC.
|
||
- Define end-to-end messaging as at-least-once delivery plus idempotent consumers/inbox. Do not
|
||
advertise generic exactly-once delivery across a database and broker.
|
||
- Separate evictable cache data from correctness-sensitive Redis data at the Redis deployment or
|
||
cluster level, not only by key prefix or database number.
|
||
- Provide capability-level safe operations and versioned Lua scripts. Do not expose a general
|
||
`RedisTemplate`, Kafka producer, HTTP client, or cloud SDK to use cases.
|
||
|
||
## 2. Scope and non-goals
|
||
|
||
This design covers:
|
||
|
||
- Redis cache, rate limiting, sessions, idempotency, locks, and reusable atomic operations;
|
||
- polling and CDC outbox, Kafka producer/consumer, inbox, delivery semantics, and query/read models;
|
||
- file server, object storage, HTTP client, notification, JPA, MongoDB, web, GraphQL, gRPC, and
|
||
WebSocket production baselines;
|
||
- provider selection, typed settings, health, observability, Gradle ownership, and test strategy;
|
||
- a phased path from the current skeleton to an operational baseline.
|
||
|
||
This design intentionally does not:
|
||
|
||
- select one infrastructure topology for every future product;
|
||
- invent domain-specific throughput or latency numbers;
|
||
- claim benchmark improvements without a real workload and environment;
|
||
- make every optional dependency active in the default application;
|
||
- promise cross-store atomicity, generic exactly-once processing, or strong consistency from a
|
||
Redis lock;
|
||
- place example business concepts in production modules;
|
||
- create a universal repository, universal query DSL, or a raw infrastructure facade in
|
||
`application-core`.
|
||
|
||
## 3. Evidence-based current state
|
||
|
||
The repository already has stronger boundaries than a typical starter, but many adapters stop at an
|
||
extension seam.
|
||
|
||
| Capability | Current evidence | Current operational gap |
|
||
| --- | --- | --- |
|
||
| Redis cache | `CacheStore` exposes only `get/put(String)` and `RedisClient` is project-supplied | No Redis SDK, TTL, delete, CAS, bulk operations, serialization policy, real health, or integration test |
|
||
| Rate limit | `RateLimitAlgorithm` has only `FIXED_WINDOW`; `FixedWindowRateLimiter` is in-process | Not sliding-window as previously assumed; not multi-node; key map has no removal policy |
|
||
| Session | `SecurityConfig` fixes JWT, CSRF disabled, and `STATELESS` | No Redis session repository, stateful security profile, rotation, shared logout, or multi-pod contract |
|
||
| Idempotency | Framework-free executor/port plus JPA implementation | Port documentation is DB-specific; no owner token; execution lease and replay TTL are conflated; Redis provider absent |
|
||
| Lock | Framework-free lock port plus local/JDBC `LockRegistry` providers | Multi-node means JDBC only; no owner token, renewal, lease-lost signal, fencing, or Redis provider |
|
||
| Outbox | JPA/PostgreSQL polling relay with claim, retry, FIFO, and metrics | PostgreSQL `SKIP LOCKED` and mutable status row are coupled to polling; no CDC mode or immutable CDC envelope |
|
||
| Kafka | `KafkaSender` is a project-supplied seam and the module has no Kafka SDK | No broker acknowledgement, producer security/tuning, consumers, inbox, retries, DLT, schema contract, or rebalance handling |
|
||
| HTTP client | Connect/read timeout, a `globalCallTimeout` label, retry, circuit breaker, buffered size bound, and diagnostics exist | The global value neither bounds nor cancels an active call; no explicit bulkhead, per-client pool controls, SSRF policy, TLS/mTLS profile, redirect policy, or OTel-owned propagation |
|
||
| Object storage | Actual filesystem and synchronous S3/MinIO adapters exist | Entire object is `byte[]`; unsafe local-oriented defaults; no multipart, presigned operation, checksum contract, encryption, lifecycle, or orphan cleanup |
|
||
| File server | CSV is built and written with JDK filesystem APIs | Whole export is buffered; overwrite is non-atomic; no fsync/rename protocol, quota, retention, symlink defense, or shared-filesystem semantics |
|
||
| Notification | Route/fan-out/fail-open framework plus Google/Slack client seams | No real provider SDK, durable delivery, template/versioning, preference, dedupe, receipt, fallback, or provider rate control |
|
||
| MongoDB | Opt-in Spring Data Mongo configuration | No production document/port contract kit, concern policy, index/migration contract, replica-set transaction test, or change-stream checkpoint |
|
||
| GraphQL | Minimal schema/controller and error resolver | No depth/complexity policy, persisted queries, DataLoader baseline, field authorization, production schema checks, or subscription policy |
|
||
| gRPC | Netty server lifecycle, health, reflection, and error mapping | No feature proto build convention, TLS/mTLS, auth, deadline enforcement, retry contract, message limits, or stream backpressure baseline |
|
||
| WebSocket | In-process domain-event to STOMP simple-broker bridge | Not cluster-safe or durable; no broker relay profile, destination auth, bounded queues, reconnect/resume, or explicit drop policy |
|
||
| Observability | Structured logging, Micrometer, OTel bridge, actuator, and selected metrics exist | Coverage is uneven; several registry entries are not wired; manual `traceparent` propagation competes with real instrumentation |
|
||
| Bootstrap composition | The default `app-bootstrap` graph omits file server, object storage, MongoDB, GraphQL, gRPC, and WebSocket leaves | Their source presence is not a runtime guarantee; blindly adding all dependencies would also activate unsafe defaults |
|
||
| Settings contract | YAML, environment-key registry, typed settings, and conditional beans are not consistently end-to-end aligned | Notification provider keys diverge, and JWT exposes JWKS/clock-skew settings while the implementation uses issuer discovery and a fixed skew |
|
||
|
||
Important correctness findings:
|
||
|
||
- `IdempotencyStorePort.complete(scope)` and `discard(scope)` cannot distinguish an expired original
|
||
owner from a new owner. A stale caller can overwrite or delete a reclaimed record.
|
||
- The multi-instance startup validator checks bean names, not capability types or guarantees. Its
|
||
test accepts plain `Object` beans, so it can report safety without a working distributed
|
||
implementation.
|
||
- The current outbox row is mutated through `PENDING/IN_FLIGHT/PUBLISHED/...`. Debezium's standard
|
||
Outbox Event Router expects the outbox event source to behave as an insert-only queue, so CDC
|
||
cannot be attached to the current mutable design without a schema/behavior split.
|
||
- The current authenticated rate-limit key lacks a route or policy dimension, while the
|
||
unauthenticated key includes the route. This makes policy isolation inconsistent.
|
||
- Blanket `catch (Exception)` cache fail-open behavior can hide codec or programming defects as
|
||
ordinary cache misses.
|
||
- Merely composing every existing leaf is unsafe: GraphQL and WebSocket lack a uniform module
|
||
enable gate, gRPC defaults include plaintext/reflection behavior, and filesystem object storage
|
||
can activate through a missing-property default.
|
||
- The HTTP decorator order currently lets the circuit breaker observe a complete retry bundle,
|
||
while its documentation says each attempt is counted. The declared global call timeout also
|
||
does not actively cancel an already-running attempt.
|
||
|
||
## 4. Alternatives considered
|
||
|
||
### A. One large infrastructure starter in `app-bootstrap`
|
||
|
||
This is rejected. It would make classpath presence activate too much auto-configuration, blur
|
||
provider ownership, and turn the composition root into an infrastructure implementation module.
|
||
|
||
### B. A new Gradle leaf for every capability-provider pair
|
||
|
||
Examples would be `cache-redis`, `lock-redis`, `session-redis`, `idempotency-redis`, and
|
||
`rate-limit-redis`. This has the cleanest physical isolation but creates module and connection
|
||
configuration duplication immediately. It also changes the exact 19-leaf registry before the
|
||
semantic contracts are stable.
|
||
|
||
This becomes appropriate only when a provider has an independent release cadence, security
|
||
boundary, deployment lifecycle, or dependency graph.
|
||
|
||
### C. Semantic ports plus existing technology leaves
|
||
|
||
This is the selected first-stage design.
|
||
|
||
- `application-core` owns semantic ports and framework-free orchestration.
|
||
- existing adapter leaves own actual SDKs and provider-specific behavior;
|
||
- `app-bootstrap` selects exactly one provider per required capability;
|
||
- provider packages use separate failure policies even when they share a client library;
|
||
- module splits are made only for direction or lifecycle reasons.
|
||
|
||
The known compromise is the `cache-redis` module name. Its short-term responsibility becomes Redis
|
||
technology capabilities, while a rename to `adapter:outbound:redis` is deferred to a separately
|
||
approved module-registry migration.
|
||
|
||
### D. Extract an external platform BOM/starter repository now
|
||
|
||
This is deferred. Extraction before the contracts and test kits are proven would freeze immature
|
||
APIs and make local architecture verification harder. A later extraction may publish provider
|
||
artifacts and a BOM after at least two real consumers validate the contracts.
|
||
|
||
## 5. Target architecture
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
WEB[Web / GraphQL / gRPC / WebSocket] --> APP[application-core use cases]
|
||
KIN[Future inbound Kafka adapter] --> APP
|
||
APP --> PORTS[Semantic outbound ports]
|
||
PORTS --> JPA[JPA/PostgreSQL providers]
|
||
PORTS --> MONGO[Mongo providers]
|
||
PORTS --> REDIS[Redis capability providers]
|
||
PORTS --> MSG[Messaging producer providers]
|
||
PORTS --> IO[HTTP / notification / storage / files]
|
||
BOOT[app-bootstrap composition root] -. selects and validates .-> WEB
|
||
BOOT -. selects and validates .-> JPA
|
||
BOOT -. selects and validates .-> MONGO
|
||
BOOT -. selects and validates .-> REDIS
|
||
BOOT -. selects and validates .-> MSG
|
||
OBS[Metrics / traces / logs / health] -. decorates runtime edges .-> BOOT
|
||
```
|
||
|
||
### Layer ownership
|
||
|
||
| Concern | Owner | Must not leak |
|
||
| --- | --- | --- |
|
||
| Domain invariant, domain event | `domain-core` | Spring, SDK, transport, persistence |
|
||
| Application-invoked cache/idempotency/lock/inbox semantic contract | `application-core` | Redis, SQL, Kafka, Servlet, Micrometer |
|
||
| Feature-specific query shape | feature application package via `*QueryPort` | JPA entity, web DTO, generic repository |
|
||
| Transport-edge rate limit and operational descriptor contracts shared across leaves | `shared-contract` | provider SDK, Servlet, Redis, and business/domain concepts |
|
||
| Redis connection, codec, scripts, cache/rate/idempotency/lock provider | `adapter:outbound:cache-redis` initially | use-case and business policy |
|
||
| Session repository implementation | Redis provider package; security behavior composed by web/bootstrap | Spring Session types in application/domain |
|
||
| JPA transaction, same-store outbox/inbox/idempotency, DB lock | `adapter:outbound:persistence-jpa` | use-case policy |
|
||
| Kafka producer | `adapter:outbound:messaging` | consumer handler and use case |
|
||
| Kafka consumer | future `adapter:inbound:messaging-kafka` | producer implementation and persistence entity |
|
||
| HTTP/security/response mapping | relevant inbound adapter | repository/client SDK |
|
||
| Provider selection, exact-one validation, health composition | `app-bootstrap` | business policy |
|
||
|
||
### New or revised core contracts
|
||
|
||
The implementation plan should introduce or revise these contracts:
|
||
|
||
- `CachePort`, `CacheRegion`, `CacheKey`, `CacheLookup`, `CacheWritePolicy`,
|
||
`CacheAsideExecutor`;
|
||
- transport-edge `EdgeRateLimitContract`, `RateLimitRequest`, and `RateLimitDecision` in
|
||
`shared-contract`; a use-case-owned `BusinessQuotaPort` belongs in `application-core` only when a
|
||
domain/application policy actually invokes it;
|
||
- owner-token-based `IdempotencyStorePort` claim/renew/complete/release;
|
||
- existing `DistributedLockPort` documented as an efficiency mutex;
|
||
- separate `FencedLockPort`, and later `LeaderElectionPort`, `SemaphorePort`, or `WorkClaimPort`
|
||
only when those semantics are needed;
|
||
- `InboxStorePort` and `MessageConsumptionExecutor`;
|
||
- richer outbox/integration-message envelope;
|
||
- `ReadConsistency` and versioned cursor values used by feature-specific query ports;
|
||
- streaming and precondition-oriented object/file contracts.
|
||
|
||
These are deliberately **not** application ports:
|
||
|
||
- a raw Redis command API;
|
||
- `RedisTemplate`, Lettuce/Jedis connections, or Spring Session repositories;
|
||
- a generic Kafka producer/consumer;
|
||
- a generic `RestClient`;
|
||
- a general cloud SDK facade;
|
||
- a generic repository or arbitrary query language.
|
||
|
||
Session is a transport/security concern. A `SessionControlPort` is added only if an application use
|
||
case must revoke a user's sessions, enforce a business-driven concurrent-session rule, or audit
|
||
session control.
|
||
|
||
## 6. Capability readiness model
|
||
|
||
Each capability has a visible readiness level:
|
||
|
||
| Level | Meaning | Required evidence |
|
||
| --- | --- | --- |
|
||
| R0 Contract | Type, seam, or placeholder only | unit tests and architecture boundary |
|
||
| R1 Local | Works in one local process or local service | focused integration test and documented limitations |
|
||
| R2 Production baseline | Real provider, safe configuration, failure semantics, health, metrics, security, graceful lifecycle | provider contract, real-service integration, concurrency/failure tests, runbook |
|
||
| R3 Scale/HA | Cluster/failover/rolling-upgrade behavior is proven | topology tests, compatibility matrix, recovery and capacity runbooks |
|
||
|
||
The current repository contains a mixture of R0, R1, and partial R2 components. Documentation and
|
||
startup diagnostics must not label an R0 seam as an R2 provider.
|
||
|
||
Every capability receives a "capability card" containing:
|
||
|
||
- owner module and semantic port;
|
||
- provider IDs and readiness level;
|
||
- guarantee and explicit non-guarantees;
|
||
- default failure mode and allowed overrides;
|
||
- required topology and persistence/eviction policy;
|
||
- configuration and secrets;
|
||
- liveness/readiness impact;
|
||
- bounded-cardinality metrics and trace spans;
|
||
- fit/non-fit guidance, cost model, and resource bounds;
|
||
- common unsafe recipes, the race/failure they create, and the safe replacement;
|
||
- focused, integration, and failure-test commands;
|
||
- rolling-upgrade and recovery notes;
|
||
- runbook links.
|
||
|
||
This is how the template teaches operational depth without forcing every project to use every
|
||
feature. R2 examples are executable contract examples, not copy-only snippets: a provider must
|
||
prove its documented concurrency and failure semantics before the card can claim the guarantee.
|
||
|
||
## 7. Provider selection and configuration
|
||
|
||
Provider selection becomes explicit per capability. `multi-instance-enabled` remains descriptive
|
||
runtime context, not an implicit provider selector.
|
||
|
||
`provider: disabled | <provider-id>` is the activation SSOT for a selectable capability. A binding
|
||
map uses `disabled | <provider-id>` per binding, and a mode capability such as outbox uses
|
||
`dispatch-mode: disabled | polling | cdc`. A leaf-level `enabled` flag exists only when there is no
|
||
provider or mode axis. If a legacy flag is temporarily retained, any disagreement with the SSOT is
|
||
a startup error.
|
||
|
||
`matchIfMissing=true`, classpath presence, or a local-provider default must never activate a
|
||
production capability. Local, in-memory, plaintext, reflection, auto-create, and filesystem
|
||
profiles are rejected in a production profile unless a deployment explicitly opts into a
|
||
documented exception.
|
||
|
||
Illustrative target configuration:
|
||
|
||
```yaml
|
||
ca-skeleton:
|
||
capabilities:
|
||
cache:
|
||
bindings:
|
||
worklog-summary: redis
|
||
rate-limit:
|
||
provider: redis
|
||
fallback: local-emergency
|
||
idempotency:
|
||
provider: jdbc
|
||
guarantee: same-store-transactional
|
||
lock:
|
||
provider: jdbc
|
||
guarantee: efficiency
|
||
outbox:
|
||
dispatch-mode: polling
|
||
messaging:
|
||
producer: kafka
|
||
consumer: disabled
|
||
security:
|
||
auth-mode: jwt
|
||
|
||
providers:
|
||
redis:
|
||
connections:
|
||
cache:
|
||
endpoint: ${REDIS_CACHE_ENDPOINT}
|
||
coordination:
|
||
endpoint: ${REDIS_COORDINATION_ENDPOINT}
|
||
session:
|
||
endpoint: ${REDIS_SESSION_ENDPOINT}
|
||
```
|
||
|
||
Credentials, keys, and certificates are secret references or environment-provided values, not
|
||
literal repository defaults.
|
||
|
||
### Capability descriptors
|
||
|
||
Each active provider contributes a typed descriptor with at least:
|
||
|
||
- `capabilityId`;
|
||
- `providerId`;
|
||
- readiness level;
|
||
- guarantee class;
|
||
- failure mode;
|
||
- multi-instance support;
|
||
- required backing role;
|
||
- readiness impact;
|
||
- implementation version.
|
||
|
||
`app-bootstrap` validates descriptors and selected settings. It does not accept arbitrary beans
|
||
with a magic name.
|
||
|
||
Examples of fail-fast topology rules:
|
||
|
||
- `redis-session` requires the session Redis role, CSRF/cookie settings, and a session repository;
|
||
- `idempotency=redis` cannot claim same-store transactional atomicity for a JDBC business write;
|
||
- `outbox=cdc` and the polling scheduler cannot both be active;
|
||
- `multi-instance=true` rejects an in-process-only required lock or rate-limit provider unless an
|
||
explicit degraded mode is selected;
|
||
- a required Kafka producer must support broker acknowledgement and bounded delivery timeout;
|
||
- a correctness capability cannot use the evictable cache Redis role;
|
||
- no enabled provider may have an unregistered health, metrics, or configuration card.
|
||
|
||
Configuration contract tests traverse the complete path:
|
||
|
||
```text
|
||
environment-key registry -> application YAML -> typed settings -> validation -> conditional bean
|
||
```
|
||
|
||
They fail on unknown/dead keys, missing registered keys, multiple active providers, inactive
|
||
provider settings that unexpectedly create beans, or a selected provider without its SDK and
|
||
health indicator.
|
||
|
||
### Failure modes
|
||
|
||
Failure policy is capability-specific, not globally "fail open" or "fail closed".
|
||
|
||
| Capability | Default failure policy |
|
||
| --- | --- |
|
||
| Optional cache | Fail open to source load; surface degraded result and metric |
|
||
| Cache codec/programming error | Fail closed for the operation; evict/quarantine corrupt entry; do not hide as miss |
|
||
| Security session store | Fail closed; authentication state must not be invented |
|
||
| Keyed mutation idempotency | Fail closed |
|
||
| Strict rate limit for abuse/cost boundary | Fail closed or a deliberately bounded local emergency limiter |
|
||
| Availability-oriented rate limit | Explicit local emergency fallback, never unlimited silent bypass |
|
||
| Efficiency lock | Fail or continue only according to declared use-case policy |
|
||
| Fenced correctness lock | Abort protected work on acquisition or lease-loss failure |
|
||
| Outbox append | Roll back the business transaction |
|
||
| Outbox dispatcher outage | Keep writes accumulating; alert on lag/backlog |
|
||
| Best-effort notification/message | Explicit fail open |
|
||
| Durable notification/message | Outbox/inbox, retry, and terminal failure path |
|
||
|
||
## 8. Redis capability design
|
||
|
||
The Redis summary in this section is expanded and governed by
|
||
[Redis Production Capability Deep Design](2026-07-26-redis-production-capability-design.md). That
|
||
document fixes the application contracts, role/deployment isolation, key and codec schemas,
|
||
versioned Function/Lua catalog, cache strategies, rate-limit algorithms, lease/fencing and
|
||
idempotency state machines, session profile, client backpressure, security, observability, and
|
||
topology/failure CI in implementation-ready detail.
|
||
|
||
### 8.1 Runtime role isolation
|
||
|
||
At minimum, Redis is modeled as three roles:
|
||
|
||
| Role | Data | Eviction/durability expectation | Typical failure semantics |
|
||
| --- | --- | --- | --- |
|
||
| `cache` | recomputable values, negative entries, soft locks | bounded memory, an `allkeys-*` policy selected by operations | usually fail open |
|
||
| `coordination` | idempotency, locks, strict quotas, fencing counters | `noeviction`, HA, persistence/capacity alarms | usually fail closed |
|
||
| `session` | authenticated sessions and indexes | `noeviction`, HA, serializer compatibility, persistence | fail closed |
|
||
|
||
A Redis database number or key prefix does not isolate `maxmemory`, eviction, failover, or noisy
|
||
neighbors. Production settings require separate managed databases, clusters, or instances where
|
||
these guarantees differ.
|
||
|
||
Each role has typed endpoint/topology/TLS/ACL/timeout/pool/topology-refresh settings and its own
|
||
health component. Application code never issues `CONFIG SET`; deployment configuration owns
|
||
`maxmemory`, eviction, persistence, and replica policy.
|
||
|
||
### 8.2 Safe operation catalog
|
||
|
||
The Redis leaf provides reusable, capability-oriented operations:
|
||
|
||
| Category | Baseline operations |
|
||
| --- | --- |
|
||
| Cache | get, multi-get, put with bounded TTL, put-if-absent, evict, multi-evict, touch, namespace-generation bump |
|
||
| Atomic primitives | set-if-absent-with-TTL, compare-and-delete, compare-and-expire, increment-with-initial-TTL |
|
||
| Rate limit | fixed window, sliding counter, token bucket; sliding log and GCRA opt-in |
|
||
| Idempotency | atomic claim, renew, complete, release with owner token |
|
||
| Lock | acquire, owner-safe renew/release, optional atomic fencing counter |
|
||
| Streaming/invalidation | bounded Redis Streams or Pub/Sub helpers only inside messaging/cache adapters |
|
||
|
||
These are adapter utilities, not use-case APIs. Raw list/set/hash commands remain available to a new
|
||
adapter implementation through the client library, but are not promoted as a stable application
|
||
contract.
|
||
|
||
Every packaged script has a `ScriptDescriptor`:
|
||
|
||
- stable name and semantic version;
|
||
- source checksum;
|
||
- key count and cluster-slot rule;
|
||
- argument/result schema;
|
||
- complexity and maximum collection size;
|
||
- timeout/failure behavior;
|
||
- metrics name;
|
||
- compatible Redis versions.
|
||
|
||
Every public helper also has an operation contract that states atomicity scope, command/script
|
||
complexity, worst-case state growth, cluster-slot constraints, clock source, retry safety, and
|
||
failure result. Its documentation pairs the common unsafe multi-command recipe with the packaged
|
||
atomic replacement, and its provider contract contains a concurrent race test. This makes the
|
||
reason for Lua or another primitive visible to a developer instead of presenting a magic helper.
|
||
|
||
### 8.3 Why Redis single-threading does not remove races
|
||
|
||
An individual Redis command is serialized, but a client sequence such as:
|
||
|
||
```text
|
||
GET -> decide -> INCR -> EXPIRE
|
||
```
|
||
|
||
is not one command. Other clients can interleave between its steps. The `INCR` may succeed while
|
||
`EXPIRE` is skipped after a client failure, or two clients can both make a decision from stale
|
||
state.
|
||
|
||
Lua scripts solve the read/decide/write atomicity problem by running as one atomic server-side
|
||
operation. They introduce another risk: Redis blocks other work while a script runs. Therefore:
|
||
|
||
- scripts are O(1) or strictly bounded;
|
||
- no unbounded loop, `KEYS`, large collection scan, network, filesystem, or dynamic code;
|
||
- keys are declared through `KEYS[]`;
|
||
- multi-key scripts use a common, narrow hash tag such as a resource hash, not a whole-tenant hot
|
||
slot;
|
||
- time arithmetic uses integer milliseconds and a documented clock source;
|
||
- scripts are classpath resources, not concatenated strings;
|
||
- the client uses cached execution (`EVALSHA`) with safe reload on `NOSCRIPT`;
|
||
- Redis Functions are an opt-in deployment mode only when function installation/version ownership
|
||
is available;
|
||
- slow-script, command-timeout, pool, and server-latency signals are monitored.
|
||
|
||
### 8.4 Cache contract and strategies
|
||
|
||
The current `String get/put` API is replaced by an application-owned cache contract that separates:
|
||
|
||
- `HIT`, `MISS`, `NEGATIVE_HIT`, and `DEGRADED`;
|
||
- region and key from backend provider;
|
||
- payload schema/version from Redis serialization;
|
||
- positive TTL, negative TTL, soft TTL, and hard TTL;
|
||
- backend failure from codec/programming failure.
|
||
|
||
The Redis adapter stores an opaque, versioned payload. A framework-free `CacheCodec<T>` and
|
||
`CacheAsideExecutor<T>` keep domain/application values type-safe without allowing the Redis adapter
|
||
to serialize arbitrary domain objects by reflection. JDK native serialization is forbidden.
|
||
|
||
Baseline strategies:
|
||
|
||
- cache-aside;
|
||
- after-commit invalidation, with eviction preferred over blind cache update;
|
||
- bounded positive/negative TTL;
|
||
- TTL jitter;
|
||
- process-local single-flight;
|
||
- versioned key namespace;
|
||
- payload size limit;
|
||
- batched `SCAN` plus `UNLINK` for operator cleanup, never regular `KEYS`;
|
||
- low-cardinality hit/miss/error/load metrics.
|
||
|
||
Opt-in strategies:
|
||
|
||
- stale-while-revalidate with soft/hard TTL;
|
||
- refresh-ahead;
|
||
- probabilistic early refresh;
|
||
- L1 local plus L2 Redis;
|
||
- distributed stampede suppression with double-check and bounded soft lock;
|
||
- client-side tracking or Pub/Sub invalidation;
|
||
- compression above a configured threshold;
|
||
- generation-based mass invalidation.
|
||
|
||
Write-through cannot imply atomic DB+Redis commit. Write-behind is not an in-memory executor behind
|
||
the cache API; it requires a durable outbox/stream consumer and its own retry/DLT semantics.
|
||
Pub/Sub invalidation is best effort, so TTL and schema version remain the recovery boundary.
|
||
|
||
### 8.5 Rate-limit strategy registry
|
||
|
||
Transport adapters resolve principal, tenant, API key, IP, route, and policy, then invoke the
|
||
framework-neutral `EdgeRateLimitContract` from `shared-contract`. The Redis provider implements
|
||
that contract and `app-bootstrap` wires the edge; `application-core` is not involved unless a
|
||
separate business quota is part of a use case. The contract receives only an opaque hashed subject
|
||
and policy:
|
||
|
||
```text
|
||
RateLimitRequest(policyId, subjectHash, cost)
|
||
-> RateLimitDecision(allowed, remaining, retryAfter, resetAt)
|
||
```
|
||
|
||
The stable key dimensions are:
|
||
|
||
```text
|
||
environment + policyId + tenant? + subjectHash
|
||
```
|
||
|
||
Raw PII, token, request body, and full URL are forbidden in Redis keys, logs, traces, and metric
|
||
tags.
|
||
|
||
| Algorithm | Operational characteristic | Availability |
|
||
| --- | --- | --- |
|
||
| Fixed window | O(1), simple, permits boundary burst | local/R1 and Redis/R2 |
|
||
| Sliding-window log | exact but one sorted-set member per event; memory/CPU grows with volume | advanced opt-in |
|
||
| Sliding-window counter | approximate sliding window with bounded O(1) state | production option |
|
||
| Token bucket | independently controls average rate and burst capacity | recommended Redis default |
|
||
| Leaky bucket | smooth output; synchronous HTTP request queueing is not allowed | background/workflow opt-in |
|
||
| GCRA | precise smoothing with compact state | advanced opt-in |
|
||
|
||
Policies are selected by stable `policyId`, not a global algorithm. A policy declares algorithm,
|
||
capacity/rate/window, burst, cost, failure mode, and subject dimensions. `Retry-After` comes from
|
||
the decision rather than the current fixed one-second value.
|
||
|
||
When Redis fails, an optional emergency limiter is conservative, process-local, bounded in size and
|
||
TTL, and emits a degraded signal. It is not described as globally accurate.
|
||
|
||
### 8.6 Session profile
|
||
|
||
Authentication modes are exclusive:
|
||
|
||
```text
|
||
jwt | redis-session
|
||
```
|
||
|
||
JWT remains the default:
|
||
|
||
- stateless resource server;
|
||
- no session repository;
|
||
- CSRF may remain disabled for bearer-only APIs;
|
||
- Redis failure does not affect authentication.
|
||
|
||
`redis-session` is opt-in:
|
||
|
||
- real Spring Session Redis implementation on the separate session role;
|
||
- `IF_REQUIRED` session creation;
|
||
- CSRF enabled and tested;
|
||
- `Secure`, `HttpOnly`, `SameSite`, path/domain, expiry, and session-id rotation settings;
|
||
- logout deletes the server session;
|
||
- explicit serializer with versioning and allowed types; no JDK serialization;
|
||
- rolling-deploy compatibility test;
|
||
- multi-pod read/touch/expiry/logout contract;
|
||
- fail-closed repository behavior and readiness inclusion;
|
||
- optional indexed repository only when principal lookup/concurrent-session control is required.
|
||
|
||
Login endpoints and identity proofing remain product decisions. The capability pack provides secure
|
||
session storage and web-security composition, not a guessed login business flow.
|
||
|
||
### 8.7 Redis health and telemetry
|
||
|
||
Required bounded metrics include:
|
||
|
||
- cache get/load/invalidate outcome and duration by logical region;
|
||
- rate-limit decision/fallback by policy and algorithm;
|
||
- idempotency claim/replay/conflict/takeover/store operation;
|
||
- lock acquire/renew/lease-lost/release/fencing rejection;
|
||
- session repository operation/expiry/error;
|
||
- Redis command latency, timeout, connection pool saturation, reconnect, and server memory/eviction.
|
||
|
||
No cache key, user, tenant, session ID, message ID, or lock resource is a metric tag.
|
||
|
||
## 9. Idempotency design
|
||
|
||
The current `find -> tryBegin` and scope-only completion API becomes one atomic ownership protocol:
|
||
|
||
```text
|
||
claim(request)
|
||
-> ACQUIRED(ownerToken, leaseUntil)
|
||
-> REPLAY(storedResponse, replayUntil)
|
||
-> IN_PROGRESS(retryAfter)
|
||
-> FINGERPRINT_MISMATCH
|
||
```
|
||
|
||
Follow-up operations:
|
||
|
||
```text
|
||
renew(ownerToken, newLeaseUntil)
|
||
complete(ownerToken, response, replayUntil)
|
||
release(ownerToken)
|
||
```
|
||
|
||
Rules:
|
||
|
||
- execution lease and completed-response replay TTL are separate;
|
||
- completion and release compare the owner token;
|
||
- stale-owner operations return ownership-lost and cannot mutate a new claim;
|
||
- fingerprint canonicalization/version is explicit;
|
||
- stored response size, encryption/PII, codec version, and allowed replay metadata are bounded;
|
||
- mutations fail closed when the idempotency store is unavailable;
|
||
- JPA and Redis providers run the same contract suite;
|
||
- the sample module must demonstrate a POST genuinely invoking the executor rather than only
|
||
accepting an unused header.
|
||
|
||
Guarantees are declared:
|
||
|
||
- `REQUEST_REPLAY`: suppresses concurrent/repeated request execution as far as the store protocol
|
||
can observe;
|
||
- `SAME_STORE_TRANSACTIONAL`: the idempotency record and business change commit in the same
|
||
datastore transaction;
|
||
- `EXTERNAL_IDEMPOTENCY`: an outbound provider also receives a stable idempotency key.
|
||
|
||
A Redis claim plus a JDBC business write is not `SAME_STORE_TRANSACTIONAL`. A crash after the
|
||
business write and before Redis completion can cause replayed execution. DB constraints,
|
||
intrinsically idempotent commands, outbox, compensation, or downstream idempotency keys remain
|
||
necessary.
|
||
|
||
## 10. Lock and coordination design
|
||
|
||
One mutex interface should not impersonate every coordination primitive.
|
||
|
||
| Contract | Purpose | Correctness expectation |
|
||
| --- | --- | --- |
|
||
| `DistributedLockPort` / future `DistributedMutexPort` | reduce duplicate work or contention | efficiency only; DB constraints/invariants remain authoritative |
|
||
| `FencedLockPort` | prevent stale holders from writing | protected resource must reject lower fencing tokens |
|
||
| `LeaderElectionPort` | select an active coordinator | explicit leadership/lease lifecycle |
|
||
| `SemaphorePort` | bound distributed concurrency | permit ownership and expiry |
|
||
| `WorkClaimPort` | claim queue/jobs | claim token and visibility timeout |
|
||
|
||
A Redis lease uses:
|
||
|
||
- `SET key ownerToken NX PX lease`;
|
||
- owner-checked renew and release Lua scripts;
|
||
- bounded acquisition retry with jitter;
|
||
- maximum hold duration and bounded renewal count;
|
||
- lease-lost signal;
|
||
- idempotent release;
|
||
- optional atomic monotonically increasing fencing counter in the same cluster slot.
|
||
|
||
Blind `DEL` is forbidden. An unlimited watchdog is forbidden. Fencing is useful only if the
|
||
database, object store, or downstream write port stores and rejects stale tokens.
|
||
|
||
The fencing counter is separate from the expiring lease and never expires or resets. The protected
|
||
resource rejects a token lower than its accepted high watermark. Equality is accepted only for the
|
||
same owner token and lease epoch; a different owner must present a strictly greater token. If Redis
|
||
failover can lose an acknowledged increment, this provider cannot claim an R2 correctness
|
||
guarantee: acquisition fails closed/unready until a monotonic epoch above the protected resource's
|
||
recorded high watermark is established. A lock plus a best-effort Redis counter is still only an
|
||
efficiency mechanism.
|
||
|
||
Provider guidance:
|
||
|
||
- local provider: development/single-node only;
|
||
- JDBC table/advisory provider: low-rate coordination close to the primary DB;
|
||
- Redis provider: low-latency coordination with explicit failover limitations;
|
||
- a consensus system may be added later for stronger lease/election requirements;
|
||
- Kafka partition ownership is a work-distribution mechanism, not a generic mutex.
|
||
|
||
Redis/Redlock is not advertised as a strong correctness guarantee. Network partitions, failover,
|
||
lease expiry, pauses, and wall-clock behavior require fencing or an authoritative invariant.
|
||
|
||
## 11. Outbox, CDC, messaging, and inbox design
|
||
|
||
### 11.1 What can and cannot be provider-neutral
|
||
|
||
The outbox append must share the source-of-truth transaction:
|
||
|
||
- a JPA/PostgreSQL business write appends to PostgreSQL;
|
||
- a Mongo business write appends to Mongo in the same supported transaction;
|
||
- moving the append to Redis or Kafka would lose atomicity unless a real distributed transaction is
|
||
introduced.
|
||
|
||
The dispatch strategy is selectable:
|
||
|
||
```text
|
||
disabled | polling | cdc
|
||
```
|
||
|
||
### 11.2 Immutable event plus delivery state
|
||
|
||
Split the current mutable row:
|
||
|
||
```text
|
||
outbox_event
|
||
immutable event envelope
|
||
|
||
outbox_delivery
|
||
polling-only claim and delivery state
|
||
```
|
||
|
||
`outbox_event` contains:
|
||
|
||
- event/message ID;
|
||
- event type and schema version;
|
||
- aggregate type, aggregate ID, and aggregate sequence/version;
|
||
- logical destination and partition key;
|
||
- content type and payload;
|
||
- occurred-at time;
|
||
- tenant when active;
|
||
- correlation and causation IDs;
|
||
- trace context allowlist.
|
||
|
||
Physical Kafka topic names are adapter configuration. Application event types do not become topic
|
||
names by convention.
|
||
|
||
Polling mode writes both rows in the business transaction and mutates only `outbox_delivery`.
|
||
Polling retains current short claim transactions, broker publish outside the DB transaction,
|
||
retry/backoff, aggregate ordering, orphan reclaim, and dead-letter behavior, with these additions:
|
||
|
||
- claim owner token on every state transition;
|
||
- aggregate sequence rather than timestamp-only order;
|
||
- broker acknowledgement deadline;
|
||
- one combined retry budget across broker client and relay;
|
||
- operator replay/requeue/skip tooling and audit;
|
||
- explicit resolution for a dead event that blocks aggregate ordering.
|
||
|
||
CDC mode:
|
||
|
||
- writes only the immutable event row;
|
||
- does not create the Java polling scheduler or polling publisher;
|
||
- uses an externally deployed Kafka Connect/Debezium connector and Outbox Event Router;
|
||
- routes event ID to a header and aggregate/partition key to the broker key;
|
||
- owns connector predicate, schema mapping, offset, snapshot, WAL/replication-slot, retention,
|
||
restart, and recovery configuration;
|
||
- monitors connector lag, retained WAL, offset progress, serialization errors, and restarts;
|
||
- does not reuse polling `PUBLISHED` status or polling lag metrics.
|
||
|
||
`outbox_event` is time/range partitioned for bounded retention. Cleanup may purge only a closed
|
||
partition whose high watermark is proven consumed by the connector checkpoint and whose replay
|
||
retention has elapsed. Cleanup never updates event rows, explicitly filters any cleanup
|
||
delete/tombstone records, alarms on table/partition growth, and is tested across connector outage,
|
||
restart, and snapshot cutover.
|
||
|
||
Polling and CDC dispatch are mutually exclusive. Switching modes requires a runbook covering write
|
||
freeze or dual-read avoidance, backlog drain, connector offset verification, and rollback.
|
||
|
||
### 11.3 Kafka producer baseline
|
||
|
||
The messaging leaf gains a real client/provider:
|
||
|
||
- acknowledgement-aware send result;
|
||
- `acks=all` and idempotent producer configuration;
|
||
- bounded delivery timeout and retry budget;
|
||
- stable key/partition ordering;
|
||
- compression/batch limits;
|
||
- TLS/SASL and secret references;
|
||
- schema serializer and compatibility validation;
|
||
- low-cardinality metrics and OTel propagation;
|
||
- readiness for required producer paths;
|
||
- graceful flush and shutdown.
|
||
|
||
Kafka producer transactions are used only for Kafka-native workflows where their boundary applies,
|
||
such as consume-process-produce with committed offsets. They do not make a database write and Kafka
|
||
publish one atomic transaction.
|
||
|
||
Best-effort `MessagePublisher` remains explicitly named and documented as best effort. Durable
|
||
business events use outbox.
|
||
|
||
### 11.4 Kafka consumer and inbox
|
||
|
||
Before consumer implementation, add an `adapter:inbound:messaging-kafka` leaf through the registry
|
||
migration workflow. Its production baseline includes:
|
||
|
||
- manual acknowledgement after application success;
|
||
- handler/schema/version allowlist;
|
||
- bounded concurrency and queues;
|
||
- pause/resume backpressure;
|
||
- rebalance and `max.poll` handling;
|
||
- bounded retry topic or delayed-retry strategy;
|
||
- poison/deserialization failure classification;
|
||
- DLT plus audited replay tooling;
|
||
- trace context restoration;
|
||
- graceful drain and shutdown;
|
||
- consumer lag/rebalance/retry/DLT metrics.
|
||
|
||
`InboxStorePort` scope is:
|
||
|
||
```text
|
||
consumerGroup + handlerName + tenant? + messageId
|
||
```
|
||
|
||
For a handler that writes a database, inbox claim/completion and the business write commit in the
|
||
same database transaction. A Redis inbox may be a fast prefilter or serve a DB-free handler, but it
|
||
cannot claim same-store atomicity for a JDBC or Mongo write.
|
||
|
||
End-to-end wording is:
|
||
|
||
```text
|
||
at-least-once delivery + idempotent consumer/inbox
|
||
```
|
||
|
||
Redis Streams may be offered later as a smaller-scale messaging provider with consumer-group,
|
||
pending-entry, reclaim, trim, and dedupe contracts. It is not treated as a drop-in Kafka clone.
|
||
|
||
## 12. Query and persistence design
|
||
|
||
### 12.1 Query progression
|
||
|
||
Keep feature-specific `*QueryPort` interfaces. Do not add one universal `QueryPort<Q, R>` or generic
|
||
repository.
|
||
|
||
Supported progression:
|
||
|
||
1. same-store aggregate read;
|
||
2. same-store projection via JPQL/JdbcTemplate/Mongo projection;
|
||
3. primary/read-replica routing;
|
||
4. separate read model populated through Kafka/CDC;
|
||
5. purpose-specific search or analytical store.
|
||
|
||
Common application values:
|
||
|
||
- opaque, signed, versioned cursor;
|
||
- bounded page size;
|
||
- allowlisted sort/filter;
|
||
- `ReadConsistency` such as `STRONG`, `READ_YOUR_WRITES`, `BOUNDED_STALENESS`, `EVENTUAL`;
|
||
- projection checkpoint and lag.
|
||
|
||
`TransactionPort.inRead()` does not silently mean "use a replica." The query's consistency policy
|
||
and request context select primary or replica. Read-after-write flows remain on primary unless a
|
||
causal/checkpoint contract proves otherwise.
|
||
|
||
### 12.2 JPA/PostgreSQL production baseline
|
||
|
||
Preserve:
|
||
|
||
- OSIV disabled;
|
||
- application-owned transaction port;
|
||
- Flyway migrations;
|
||
- persistence exception translation;
|
||
- current polling outbox, idempotency, and JDBC lock providers as selectable providers.
|
||
|
||
Add:
|
||
|
||
- explicit pool sizing, acquisition timeout, leak detection policy, and shutdown;
|
||
- statement/query/lock timeout hierarchy within the request deadline;
|
||
- batch write and fetch-size settings;
|
||
- N+1 detection and representative query-plan tests;
|
||
- optimistic version and bounded pessimistic-lock use;
|
||
- primary/read-replica routing with explicit consistency;
|
||
- migration expand/contract and rollback/roll-forward rules;
|
||
- tenant filter/index/unique-constraint rules when tenancy is active;
|
||
- slow query and pool saturation metrics;
|
||
- same-store inbox implementation;
|
||
- provider packages that make PostgreSQL-specific SQL visible and tested.
|
||
|
||
Database-backed outbox/idempotency/lock remain valid providers. They stop being the only providers.
|
||
|
||
### 12.3 MongoDB production baseline
|
||
|
||
The Mongo leaf remains free of example business documents and gains reusable infrastructure:
|
||
|
||
- typed URI/topology/TLS/credential/timeout/pool settings;
|
||
- explicit read preference, read concern, write concern, and transaction options;
|
||
- replica-set/sharded-cluster requirement validation for transactions/change streams;
|
||
- index manifest, unique/TTL indexes, drift detection, and migration runner;
|
||
- schema validation and optimistic versioning guidance;
|
||
- bounded query/page/time limits;
|
||
- retryable read/write classification;
|
||
- change-stream resume token/checkpoint store and oplog-window monitoring;
|
||
- same-store Mongo outbox/inbox option;
|
||
- real replica-set Testcontainers contract;
|
||
- rolling serializer/schema compatibility.
|
||
|
||
Change streams are resumable only while the required oplog history and compatible pipeline/options
|
||
remain available. Pool sizing accounts for long-lived change-stream cursors.
|
||
|
||
## 13. Remaining outbound capability baselines
|
||
|
||
### 13.1 HTTP client
|
||
|
||
The implementation-level authority for this capability is
|
||
[HTTP Client Production Capability Deep Design](2026-07-27-httpclient-production-capability-design.md).
|
||
This subsection remains the cross-capability baseline; where detail differs, the dedicated design
|
||
governs.
|
||
|
||
Preserve the current connect/read timeout intent, bounded-response intent, retry/circuit-breaker
|
||
seams, shutdown guard, and diagnostics as characterization inputs, not as proven guarantees. The
|
||
dedicated audit shows that the current `globalCallTimeout` only gates whether another retry may
|
||
start; it does not actively bound or cancel DNS, pool wait, connect, TLS, write, response body, or
|
||
backoff. It also shows that the documented decorator order differs from the code. If every physical
|
||
attempt must affect circuit-breaker state, retry repeats a circuit-breaker-wrapped attempt; the
|
||
logical-call deadline and concurrency bulkhead remain outside that loop. If a provider intentionally
|
||
measures one logical call instead, that is a different named policy and test suite, not an accidental
|
||
wrapper-order side effect.
|
||
|
||
Add:
|
||
|
||
- named client registry with per-dependency settings;
|
||
- explicit connection pool total/per-route limits, acquisition timeout, idle eviction, DNS policy,
|
||
and graceful close;
|
||
- bulkhead and optional outbound rate limit;
|
||
- retry only for declared safe/idempotent operations, with exponential jitter and
|
||
`Retry-After` handling;
|
||
- a single total deadline covering pool wait, attempts, backoff, and body read, with active
|
||
cancellation of the engine call and response stream when the budget expires;
|
||
- redirect disabled by default or host-allowlisted;
|
||
- scheme/host/port/CIDR allowlist and DNS rebinding/SSRF defense;
|
||
- TLS trust, hostname verification, mTLS, proxy, and certificate rotation;
|
||
- request/response header and body-size allowlists;
|
||
- upload/download streaming and cancellation;
|
||
- OTel instrumentation owns trace propagation. Remove the manual `traceparent` writer when the
|
||
real tracer is active;
|
||
- failure injection and pool-exhaustion tests.
|
||
|
||
Use cases continue to depend on feature-specific anti-corruption ports such as `RepoStatsPort`, not
|
||
on `OutboundHttpClient`.
|
||
|
||
### 13.2 Notification
|
||
|
||
Split technical routing from business consent/preferences.
|
||
|
||
Application intent contains:
|
||
|
||
- channel;
|
||
- logical template ID and version;
|
||
- locale;
|
||
- recipient reference/address;
|
||
- typed template parameters;
|
||
- delivery mode and idempotency key;
|
||
- correlation/tenant context.
|
||
|
||
Application/domain policy owns consent, preference, and quiet-hour decisions when those are
|
||
business rules. The adapter owns:
|
||
|
||
- real provider clients;
|
||
- template rendering/versioning/localization;
|
||
- priority/fallback/fan-out routing;
|
||
- provider quotas and bounded retry;
|
||
- dedupe and provider idempotency key;
|
||
- durable mode through outbox/message;
|
||
- webhook signature verification and delivery receipts through an inbound adapter;
|
||
- bounce/suppression handling;
|
||
- PII-safe logs, encrypted queue content, and retention;
|
||
- per-provider health and delivery outcome metrics.
|
||
|
||
Critical notification is never routed through the current unconditional fail-open path. Best-effort
|
||
and durable interfaces are explicit.
|
||
|
||
### 13.3 Object storage
|
||
|
||
The authoritative implementation-level design for this capability is
|
||
[Object Storage Production Capability Deep Design](2026-07-28-objectstorage-production-capability-design.md).
|
||
Its ordered RED–GREEN execution batches and promotion gates are in the
|
||
[Object Storage Production Capability Implementation Plan](../plans/2026-07-28-objectstorage-production-capability.md).
|
||
This subsection is only the cross-capability baseline; the dedicated design governs when details
|
||
differ.
|
||
|
||
Replace whole-object `byte[]` as the only path with:
|
||
|
||
- streaming upload/download and range reads;
|
||
- metadata/head contract;
|
||
- checksum algorithm/value contract and verification;
|
||
- conditional create/update/delete using version/ETag preconditions;
|
||
- presigned upload/download request with bounded expiry, content type, and size;
|
||
- multipart start/upload/complete/abort and orphan cleanup;
|
||
- server-side encryption and KMS settings;
|
||
- TLS/endpoint/region/credential-chain validation;
|
||
- lifecycle/versioning/retention policy checks;
|
||
- quarantine/malware-scan hook before publish;
|
||
- payload and metadata limits;
|
||
- metrics, tracing, and retry classification.
|
||
|
||
Local filesystem and S3/MinIO pass the same semantic contract where the backend can support it.
|
||
Provider-specific optional capabilities are reported explicitly rather than silently emulated.
|
||
|
||
Production defaults do not point to local MinIO, auto-create buckets, use static credentials, or
|
||
return internal filesystem paths to clients.
|
||
|
||
Database state and object storage cannot share one local transaction. Workflows such as an image
|
||
attachment therefore use an explicit staged lifecycle:
|
||
|
||
```text
|
||
stage upload -> verify checksum/scan -> commit attachment metadata -> finalize visibility
|
||
```
|
||
|
||
Failure paths use idempotent compensation plus an orphan reconciler with retention and audit
|
||
evidence. A use case must not perform an irreversible object write inside a database transaction
|
||
and assume rollback covers both systems.
|
||
|
||
### 13.4 File server
|
||
|
||
The authoritative implementation-level design for this capability is
|
||
[Fileserver Production Capability Deep Design](2026-07-26-fileserver-production-capability-design.md).
|
||
This subsection is only the cross-capability baseline; the dedicated design governs when details
|
||
differ.
|
||
|
||
Replace the current whole-file `StringBuilder` and direct overwrite with:
|
||
|
||
- streaming row writer/iterator;
|
||
- temporary file in the target directory;
|
||
- restrictive creation permissions;
|
||
- flush/fsync file, atomic rename when supported, and directory fsync where required;
|
||
- explicit fallback when the filesystem cannot guarantee atomic move;
|
||
- no-follow-link and real-path containment checks;
|
||
- overwrite/precondition policy;
|
||
- checksum and manifest;
|
||
- size/row/disk-space quota;
|
||
- retention/reaper and partial-file cleanup;
|
||
- filename/extension/content policy;
|
||
- spreadsheet-formula injection defense for CSV/tabular exports, with a tested escaping policy;
|
||
- optional encryption and malware scan;
|
||
- NFS/SFTP-specific locking, visibility, and rename semantics documented as provider capabilities.
|
||
|
||
An exported file is identified by an opaque receipt. Absolute server paths are not public API
|
||
values.
|
||
|
||
### 13.5 Identifier and support
|
||
|
||
`adapter:outbound:identifier` continues to implement domain/application identifier ports. It may
|
||
offer random UUID and time-ordered ID providers, but ordering, clock rollback, collision, encoding,
|
||
and database-index tradeoffs are explicit. Pseudonymization keys support secret rotation and never
|
||
become reversible identifiers.
|
||
|
||
`adapter:outbound:support` remains a small home for provider-neutral outbound decorators and
|
||
diagnostic helpers. It does not become a miscellaneous infrastructure module. Fail-open decorators
|
||
classify expected dependency failures and do not swallow programming/codec/invariant defects.
|
||
|
||
## 14. Inbound transport baselines
|
||
|
||
### 14.1 Web
|
||
|
||
Preserve current validation, error envelope, authz, pagination/cursor, conditional request, OpenAPI,
|
||
request correlation, and safe cache-control foundations. Add:
|
||
|
||
- exclusive JWT/session authentication profiles;
|
||
- Redis-backed transport-edge rate-limit contract and policy registry;
|
||
- actual keyed idempotency executor integration;
|
||
- trusted-proxy chain validation;
|
||
- request/header/body/multipart limits;
|
||
- request deadline and cancellation propagation;
|
||
- graceful drain;
|
||
- stable API version/deprecation policy;
|
||
- OpenAPI compatibility gate;
|
||
- CSRF/session cookie tests for stateful mode;
|
||
- route-level security/rate/idempotency capability declarations.
|
||
|
||
### 14.2 GraphQL
|
||
|
||
Production baseline:
|
||
|
||
- shared authentication/tenant context;
|
||
- operation and field authorization;
|
||
- parser character/token/rule-depth limits;
|
||
- query depth and cost/complexity instrumentation;
|
||
- persisted-query allowlist profile;
|
||
- DataLoader/batch-loader convention and N+1 contract;
|
||
- cursor connection and bounded page policy;
|
||
- sanitized error extensions;
|
||
- introspection/GraphiQL production policy;
|
||
- schema snapshot/breaking-change check;
|
||
- query duration/complexity/error metrics;
|
||
- subscription transport delegated to an explicitly designed WebSocket/messaging path.
|
||
|
||
### 14.3 gRPC
|
||
|
||
Production baseline:
|
||
|
||
- protobuf generation/versioning convention and compatibility check;
|
||
- TLS/mTLS and service/method authorization interceptors;
|
||
- required client deadlines and server cancellation propagation;
|
||
- request/response and metadata size limits;
|
||
- retry policy only for suitable status/method semantics;
|
||
- keepalive coordinated with infrastructure;
|
||
- unary and streaming backpressure/cancellation;
|
||
- standard health status updated during startup/drain/shutdown;
|
||
- reflection opt-in outside production;
|
||
- graceful shutdown and in-flight drain;
|
||
- OTel RPC semantic spans and bounded metrics.
|
||
|
||
### 14.4 WebSocket
|
||
|
||
The simple in-memory STOMP broker remains local/R1 only.
|
||
|
||
Production baseline:
|
||
|
||
- authenticated handshake and re-auth/session-expiry behavior;
|
||
- destination-level subscribe/send authorization;
|
||
- trusted origins and payload/frame limits;
|
||
- heartbeat and idle timeout;
|
||
- bounded inbound/outbound executors, queues, send time, and an explicit disconnect/drop policy;
|
||
- sequence/resume contract where message loss matters;
|
||
- per-session ordering only when required and measured;
|
||
- broker relay or a durable integration-event bridge for multi-node delivery;
|
||
- broker availability/readiness and graceful disconnect;
|
||
- no direct serialization of arbitrary domain events to public destinations.
|
||
|
||
Cross-node durable live updates consume an integration/presentation event. The in-process Spring
|
||
event bus is not a durable or cluster-wide transport.
|
||
|
||
## 15. Observability and operational safety
|
||
|
||
### Signals
|
||
|
||
- Traces: inbound server, application use case, DB/Redis, messaging producer/consumer, HTTP, object
|
||
storage, notification, and background-worker spans with standard semantic conventions.
|
||
- Metrics: request/dependency latency, errors, saturation, backlog/lag, lease loss, retry, DLT,
|
||
cache behavior, and provider lifecycle.
|
||
- Logs: stable structured schema correlated with trace/span IDs.
|
||
- Audit: a separate durable, access-controlled record for security/business actions; not ordinary
|
||
application logs.
|
||
|
||
Instrumentation uses one context-propagation owner per transport. Payloads, tokens, Redis keys,
|
||
session IDs, raw principals, email addresses, and object names are not added to metrics and are
|
||
allowlisted or pseudonymized in logs/traces.
|
||
|
||
Production tracing includes a configured OTLP exporter and batch span processor; tests use an
|
||
in-memory exporter to prove spans and propagation rather than treating a registry entry as emitted
|
||
telemetry. Metrics similarly prove recording, tags, and cardinality. Unmatched or templating-failed
|
||
HTTP requests use a fixed route label such as `UNKNOWN`, never a raw URI.
|
||
|
||
### Health
|
||
|
||
| Probe | Rule |
|
||
| --- | --- |
|
||
| Liveness | JVM/process ability only; never DB, Redis, Kafka, SMTP, object storage, or HTTP dependencies |
|
||
| Readiness | enabled providers marked required for this deployment |
|
||
| Component health | every enabled provider, including optional cache and notification |
|
||
| Startup | configuration, migration, script/schema compatibility, and required topology validation |
|
||
|
||
Optional cache failure does not restart or necessarily unready the pod. Session, strict
|
||
idempotency, required lock, or required message publisher failure can make the application
|
||
unready. The capability descriptor decides; bean name presence does not.
|
||
|
||
### Capacity and runbooks
|
||
|
||
Each R2/R3 capability includes capacity inputs rather than fabricated numbers:
|
||
|
||
- key/message/session/object size;
|
||
- operation rate and concurrency;
|
||
- retention/TTL;
|
||
- retry amplification;
|
||
- connection/thread/partition counts;
|
||
- replica/failover expectations;
|
||
- alert thresholds derived from an actual SLO.
|
||
|
||
Required runbooks cover backlog, lag, DLT, stale lease, Redis memory/noeviction, session outage,
|
||
connector slot/WAL growth, index drift, multipart orphan, disk capacity, certificate expiry, and
|
||
provider credential rotation.
|
||
|
||
## 16. Gradle and dependency design
|
||
|
||
Rules:
|
||
|
||
- `domain-core`, `application-core`, and `shared-contract` keep project-only production
|
||
dependencies and no Spring starter/SDK.
|
||
- A real provider dependency lives only in its owning adapter leaf.
|
||
- `implementation` is the default. `api` is used only when a public contract intentionally exposes
|
||
a third-party type, which these ports generally forbid.
|
||
- Spring Boot-managed coordinates use the Boot BOM. Non-Boot SDKs import a provider BOM at module
|
||
scope, following the existing gRPC/AWS pattern.
|
||
- Dependency locks and verification metadata change in the same implementation slice as the
|
||
dependency.
|
||
- Testcontainers, Toxiproxy, embedded brokers, and schema test tools remain test/integration-test
|
||
dependencies.
|
||
- Provider contract kits use Gradle test fixtures or a dedicated test-support source set without
|
||
becoming production dependencies.
|
||
- Integration tests receive a separate `integrationTest` task per provider; architecture checks
|
||
remain part of `check`.
|
||
- Do not introduce a version catalog solely for this work. The existing BOM/module pin model can be
|
||
retained until dependency ownership itself becomes hard to maintain.
|
||
- Tighten registry edges after implementation. Do not add speculative adapter-to-adapter edges.
|
||
|
||
Expected dependency ownership:
|
||
|
||
| Dependency family | Owner |
|
||
| --- | --- |
|
||
| Spring Data Redis/Lettuce and Spring Session Redis | Redis provider leaf |
|
||
| Kafka client/Spring Kafka producer | outbound messaging leaf |
|
||
| Kafka listener runtime | future inbound messaging leaf |
|
||
| Debezium/Kafka Connect | deployment/integration-test assets, not application-core |
|
||
| Mongo driver/Spring Data Mongo | Mongo persistence leaf |
|
||
| AWS S3 SDK | object-storage leaf |
|
||
| Resilience4j/HTTP engine | HTTP-client leaf |
|
||
| gRPC/protobuf runtime/build tooling | gRPC leaf |
|
||
| OTel/Micrometer exporter/composition | bootstrap and provider instrumentation adapters |
|
||
|
||
The new Kafka inbound leaf is the only module addition proposed as structurally necessary in this
|
||
design. It requires an explicit `modules.json`, settings, Gradle dependency-gate, documentation,
|
||
and architecture-test migration rather than bypassing the exact-19 assertion.
|
||
|
||
## 17. Verification and CI design
|
||
|
||
### Test layers
|
||
|
||
| Layer | Purpose |
|
||
| --- | --- |
|
||
| Pure unit | policy, algorithms, codec/version/key rules, retry/deadline math |
|
||
| Port contract | common required-semantics suite plus guarantee/capability-specific provider suites |
|
||
| Real-service integration | Redis, PostgreSQL, Mongo replica set, Kafka, MinIO, provider sandbox |
|
||
| Concurrency | duplicate claim, token spend, stale release, ordering, session sharing |
|
||
| Failure injection | timeout, disconnect, pool exhaustion, restart, failover, network partition |
|
||
| Compatibility | serialization, schema, migration, rolling version, Redis/Kafka/Mongo version |
|
||
| Architecture | SDK/type/dependency direction and optional-provider gating |
|
||
| Operational | health, metrics cardinality, trace propagation, secret/PII absence, graceful shutdown |
|
||
|
||
Redis tests include standalone and cluster slot behavior, script reload, token mismatch, maxmemory
|
||
separation, and multiple client connections. CDC tests run PostgreSQL, Kafka, Kafka Connect/Debezium
|
||
end to end. Session tests use two application contexts against one Redis service.
|
||
|
||
Providers in different guarantee classes are never certified as semantically identical. For
|
||
example, local/JDBC/Redis locks and filesystem/S3 storage share only the required contract subset;
|
||
fencing, conditional writes, multipart, durability, and failover claims require their own
|
||
capability suite.
|
||
|
||
### CI profiles
|
||
|
||
- PR gate: unit, architecture, provider contract, and one supported real-service baseline.
|
||
- Production-readiness gate: Docker/services are required; absence is a failure, not a silent skip.
|
||
- Nightly/weekly matrix: supported datastore/broker versions, cluster/failover, rolling
|
||
serialization, Toxiproxy, and longer concurrency/soak tests.
|
||
- Optional provider sandbox tests use explicit credentials and remain separated from deterministic
|
||
local protocol tests.
|
||
- Performance tests establish product-specific budgets later. This design requires load-test
|
||
hooks and capacity metrics, not generic benchmark claims.
|
||
|
||
## 18. Phased implementation roadmap
|
||
|
||
### Phase 0 — Correctness contracts and truthful capability topology
|
||
|
||
- replace bean-name multi-instance checks with typed provider descriptors;
|
||
- remove or correct registry/settings claims for capabilities that do not exist;
|
||
- reconcile notification and JWT keys across the environment registry, YAML, typed settings, and
|
||
actual conditional beans;
|
||
- establish uniform explicit module/provider activation and prohibit missing-property activation
|
||
of local, plaintext, reflection, or auto-create defaults;
|
||
- publish a truthful default bootstrap capability manifest instead of equating source modules with
|
||
composed runtime features;
|
||
- correct and contract-test HTTP retry/circuit-breaker ordering and make the total deadline cancel
|
||
in-flight work;
|
||
- revise idempotency around owner token, lease, and replay TTL;
|
||
- distinguish efficiency lock, fenced lock, leadership, semaphore, and work claim;
|
||
- define capability cards, readiness levels, settings prefix, failure policy, and common contract
|
||
test kit;
|
||
- fix the local fixed-window key lifecycle or mark it dev-only with bounded storage;
|
||
- keep application-core dependency purity and all architecture gates green.
|
||
|
||
Acceptance: the template cannot start in a configuration that claims an unavailable or weaker
|
||
provider guarantee.
|
||
|
||
### Phase 1 — Real Redis foundation and cache
|
||
|
||
- real Spring Data Redis/Lettuce client;
|
||
- cache/coordination/session role settings and connections;
|
||
- TLS/ACL/timeouts/pool/topology/health;
|
||
- versioned codec/key schema;
|
||
- cache get/put/evict/bulk/TTL/negative result;
|
||
- cache-aside, jitter, single-flight, after-commit invalidation;
|
||
- Redis standalone/cluster/failure/observability tests.
|
||
|
||
Acceptance: cache reaches R2 while correctness Redis roles remain inactive unless selected.
|
||
|
||
### Phase 2 — Distributed rate limit, idempotency, locks, and sessions
|
||
|
||
- shared transport-edge rate-limit contract plus fixed/sliding-counter/token-bucket Lua providers;
|
||
- policy registry and emergency fallback;
|
||
- Redis/JPA idempotency contract implementations;
|
||
- JDBC/Redis lock provider selection, owner-safe renew/release, fenced lock;
|
||
- JWT/Redis-session exclusive profiles and multi-pod session contract.
|
||
|
||
Acceptance: each selected provider has explicit guarantees and failure behavior; no correctness data
|
||
uses the evictable cache role.
|
||
|
||
### Phase 3 — Kafka, polling outbox evolution, inbox, and CDC
|
||
|
||
- acknowledgement-aware real Kafka producer;
|
||
- immutable `outbox_event` plus polling `outbox_delivery`;
|
||
- claim token and aggregate sequence;
|
||
- add inbound Kafka leaf and inbox executor/provider;
|
||
- retry/DLT/replay/backpressure/graceful lifecycle;
|
||
- Debezium connector/deployment assets and end-to-end CDC profile;
|
||
- polling/CDC exclusivity and transition runbook.
|
||
|
||
Acceptance: both dispatch modes independently satisfy at-least-once delivery and idempotent-consumer
|
||
contracts without an exactly-once claim.
|
||
|
||
### Phase 4 — HTTP, notification, object storage, and file server
|
||
|
||
- complete HTTP pool/bulkhead/SSRF/TLS/OTel baseline;
|
||
- durable notification intent, provider routing, templates, receipts;
|
||
- streaming/multipart/presigned/checksum/encryption storage contracts plus staged finalization,
|
||
compensation, and orphan reconciliation;
|
||
- atomic streaming file exports, quotas, retention, CSV formula defense, and
|
||
filesystem-provider semantics.
|
||
|
||
Acceptance: each adapter has a real R2 provider, failure injection, health, metrics, and a capability
|
||
card.
|
||
|
||
### Phase 5 — JPA/Mongo query models and inbound transports
|
||
|
||
- read consistency and replica routing;
|
||
- same-store and separate read-model profiles with checkpoint/lag;
|
||
- Mongo concerns/indexes/migrations/transactions/change streams;
|
||
- GraphQL complexity/DataLoader/schema gates;
|
||
- gRPC TLS/auth/deadline/streaming/proto gates;
|
||
- WebSocket broker relay/backpressure/auth/cluster behavior;
|
||
- route/operation capability declarations across transports.
|
||
|
||
Acceptance: query and transport choices are explicit and operationally observable without leaking
|
||
transport or persistence types into core.
|
||
|
||
### Phase 6 — R3 scale and extraction review
|
||
|
||
- failover, rolling upgrade, version matrix, recovery drills, and capacity runbooks;
|
||
- evaluate splitting Redis capability-provider leaves;
|
||
- evaluate extracting a platform BOM/starter only after multiple real consumers validate the APIs.
|
||
|
||
## 19. Completion criteria for the future implementation
|
||
|
||
The implementation is complete only when:
|
||
|
||
- every enabled capability has a real provider rather than a project-supplied seam;
|
||
- provider selection is exact, typed, and fail-fast;
|
||
- the provider guarantee and non-guarantees are visible;
|
||
- unused capabilities have no runtime side effects;
|
||
- core modules remain framework/SDK-free;
|
||
- every provider passes reusable contract plus real-service/failure tests;
|
||
- correctness and optimization data stores are separated where eviction/failure semantics differ;
|
||
- health, metrics, traces, logs, graceful lifecycle, security, and runbook are present;
|
||
- CI has a non-skipping production-readiness path;
|
||
- no cross-database exactly-once or strong Redis-lock claim appears in code or documentation.
|
||
|
||
## 20. Primary references
|
||
|
||
- [Redis scripting and atomic blocking semantics](https://redis.io/docs/latest/develop/programmability/eval-intro/)
|
||
- [Redis rate-limiter use case and algorithm options](https://redis.io/docs/latest/develop/use-cases/rate-limiter/)
|
||
- [Redis key eviction](https://redis.io/docs/latest/develop/reference/eviction/)
|
||
- [Redis distributed locks and fencing guidance](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/)
|
||
- [Spring Data Redis scripting](https://docs.spring.io/spring-data/redis/reference/redis/scripting.html)
|
||
- [Spring Session Redis APIs](https://docs.spring.io/spring-session/reference/api.html)
|
||
- [Debezium Outbox Event Router](https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html)
|
||
- [Apache Kafka delivery semantics and transactions](https://kafka.apache.org/42/design/design/)
|
||
- [Apache Kafka producer configuration](https://kafka.apache.org/41/configuration/producer-configs/)
|
||
- [Resilience4j fault-tolerance primitives](https://resilience4j.readme.io/docs/getting-started)
|
||
- [Amazon S3 object-integrity checks](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity-upload.html)
|
||
- [MongoDB read concern](https://www.mongodb.com/docs/manual/reference/read-concern/)
|
||
- [MongoDB write concern](https://www.mongodb.com/docs/manual/reference/write-concern/index.html)
|
||
- [MongoDB change streams](https://www.mongodb.com/docs/manual/changestreams/)
|
||
- [GraphQL Java query limits](https://graphql-java.com/documentation/limits/)
|
||
- [gRPC deadlines](https://grpc.io/docs/guides/deadlines/)
|
||
- [gRPC retry](https://grpc.io/docs/guides/retry/)
|
||
- [Spring WebSocket external broker relay](https://docs.spring.io/spring-framework/reference/6.2/web/websocket/stomp/handle-broker-relay.html)
|
||
- [OpenTelemetry signals and semantic conventions](https://opentelemetry.io/docs/concepts/)
|