Stateless Service Design
Purpose
Decide which state may stay instance-local and what replacement requires. “Stateless” does not
mean empty memory; it means request correctness/routing does not depend on a particular
instance's volatile history. Classify state by authority, durability, consistency scope,
reconstruction source/time and loss consequence. A local derivable cache can stay; a replicated
stateful actor/broker can also be correct, but it needs explicit ownership and recovery rather
than interchangeable stateless routing.
The failure this prevents is the bug that cannot appear in any environment you have.
replicas: 1 passes every test, because with one replica the process is the shared store.
The counter, the idempotency map, the scheduled job and the local cache stay correct until
capacity is added, and then they are wrong quietly — a duplicate charge, a limit enforced at
N times its value, a job that emails everyone twice.
Workflow
- Apply loss, divergence and recovery tests. If this JVM disappears, what correctness,
accepted work, security decision, user journey or SLO changes? Can another instance rebuild
from durable truth within RTO/RPO, and can copies diverge? Authoritative state may move to a
shared store or become partitioned/replicated state with an explicit owner.
- Inventory before you redesign. Enumerate singleton bean fields,
static collections,
HttpSession attributes, caches, scheduler and executor queues, local files and
long-lived connections; classify each as derivable, per-request or authoritative. The
table and the grep shapes are in references/state-inventory.md.
- Place session state deliberately. Sticky routing, an external session store and a
signed token are three different failure and revocation profiles, not three flavours of
one idea. See
references/session-placement.md.
- Name the new authority and guarantee—database row, durable queue/outbox, replicated
partition or client token. Product labels do not decide semantics: Redis can be a cache or
configured data store; verify eviction, persistence, replication, consistency, backup and
failover before assigning authority.
- Hunt singleton assumptions. Plain application-context schedulers/startup hooks run per
replica. Fleet-once work needs partitioning, a scheduler with documented coordination, or
leader-election; TTL leases, session locks and durable job claims have different stale-
owner/recovery semantics.
- Exercise multiple replicas. Route named steps deliberately to different instances
(random balancing may miss the transition), overlap concurrent requests, then kill/restart
one during work and deployment. A green run at
replicas: 1 does not establish cross-replica correctness. Fault injection belongs in an
isolated or already authorized environment; successful cases cover only the paths exercised.
- Check the next ceiling before celebrating. Replication moves the bottleneck to what
the replicas share.
replicas × maximumPoolSize is a number the database has an opinion
about; that arithmetic is connection-pool-sizing.
Decision block
Make the instance stateless and scale by replication when:
- every request's inputs are in the request plus a shared store, and the per-request
working set is small enough to fetch inside the latency budget
- any instance may handle any key on the write path, with no ordering requirement the
storage engine does not already provide
Keep the state in the process when:
- it is derivable from an authoritative source and its loss costs only latency (a cache)
- its lifetime is one request (a transaction, a request-scoped bean, a ScopedValue binding)
Prefer partitioning by key (sharding-and-partitioning) instead when:
- the per-key working set is too large or too hot to load per request, or the key needs
single-writer ordering that shared storage would otherwise have to serialise
Prefer leader election (leader-election) instead when:
- the work must happen once per interval across the fleet rather than once per instance
(election coordinates ownership; durable claims/idempotent effects are still needed where
retries, failover or stale owners can repeat work)
Rules
- Identify which copy owns the decision and can recover it. A field's size or lifetime does
not establish authority; inspect loss, divergence and recovery together, including caches
that temporarily influence security or correctness.
- Mutable
static state on the request path needs a scope and authority check. Local metrics,
protective limits and derivable caches can be valid; a local source of truth for a fleet-wide
decision is unsafe. Inspect readers, writers, thread safety and divergence consequences.
- An uncoordinated in-process counter that gates a fleet-wide business decision enforces a
separate budget per instance. Maximum aggregate allowance can approach N× under spread,
though routing/skew changes observed behavior. Per-instance protective limits are valid when
explicitly scoped (
rate-limiting-and-load-shedding).
- Plain Spring
@Scheduled runs once per application context. With one context per replica it
runs N times unless an outer scheduler/claim/lease or idempotent work changes semantics.
- An in-memory idempotency map deduplicates only the requests that land on the same
instance and retention window. Cross-replica/restart guarantees need an atomic shared claim
and effect/recovery protocol, such as a durable unique-key record;
idempotency
owns the mechanics, this skill owns noticing that the map was never shared.
- A local cache can diverge after update/invalidation for its refresh/eviction/restart horizon;
no TTL makes staleness unbounded unless explicit invalidation or replacement succeeds, not
mathematically permanent. Cache design is
caching-strategies; the multi-replica consequence is here.
- Trace local paths to actual mounts. Container writable layers can be lost on container
replacement;
emptyDir survives container restarts but ends with the Pod; persistent volumes
have separate retention and access rules. Durability alone does not make a file reachable
from another replica. A staged upload referenced by a later request needs that contract.
HttpSession is in-process state by default. Anything in it a user would notice losing —
cart contents, a multi-step form, an authorisation decision — needs a loss/staleness policy;
it may be authoritative or reconstructible from another authority. Spring
Session changes the store without changing the servlet API: a placement change, not a
rewrite.
- Sticky sessions give affinity, not a guarantee. Affinity ends when the replica dies,
when a rolling update drains it, when the client drops the cookie, or when the balancer's
table is rebuilt. Each of those is user-visible if the state existed only there.
- A signed token moves claims to the client; signing provides integrity/authenticity, not
confidentiality. Short expiry bounds token lifetime; revocation before expiry needs a
verifier-enforced mechanism such as introspection, denylist/session version, or key/policy
changes—each trades latency,
blast radius and freshness. JWT is a format, not a session architecture.
- A WebSocket, SSE stream or long poll pins one user to one instance for the connection's
lifetime. Pushing to that user from another replica needs a broker or a fan-out, and a
replacing instance can terminate its streams. Clients need bounded reconnect and a
cursor/replay or snapshot protocol when missed events affect correctness.
- Do not claim statelessness because a class has no fields. State hides in the framework
too: session attributes, a
ThreadLocal never cleared, a filter's cache, a library's
static registry. Use the inventory and targeted failure evidence; code shape alone does not.
Stateful is not a defect
Prefer explicit stateful ownership when locality, single-writer order or working-set cost
requires it. Then specify partition placement, replication/quorum, durable log/snapshot,
ownership epochs/fencing, failover/rebalance and backup/restore. Calling that service stateless
because an orchestrator can restart it erases its hardest contract.
Security and shutdown
Inspect deployed Java, Spring/Session/Data Redis versions, storage mounts and routing before
changing placement. No upgrade is implied; ScopedValue is final in Java 25 and preview/incubator
in earlier supported releases. Missing recovery or durability evidence is unknown. Deliver the
state inventory, chosen authority/loss contract, checks performed and remaining failure cases.
- Session/auth store failure must fail closed for protected actions. A separately authorized
public/read-only degraded mode is possible; never reinterpret unknown authentication as
authenticated.
- Stop admission, durably hand off accepted queues/uploads, drain connections and only then
terminate. “No fields” does not prevent loss of in-flight accepted work.
- Bind token/session to issuer, audience, tenant and key version; protect against fixation,
replay, key rotation overlap and cross-tenant cache keys.
References
- In-process state inventory — every kind of in-process
state with its classification, the failure it produces at
replicas > 1, the grep or code
shape that finds it, and where it moves. Read when auditing a service before scaling it
out, or when a bug appears on some replicas and not others.
- Where session state lives — sticky routing, an external
store and a signed token compared on replica-death behaviour, deploy behaviour, per-request
latency and revocation, with the Spring Session and token shapes and a decision block.
Read when the service holds a session, or when a deploy logs users out.
1---2name: stateless-service-design3description: Making a service instance disposable so replicas are interchangeable: what stateless actually means — no correctness/routing dependency on one instance's volatile history; the in-process state inventory; and session state as a placement decision between sticky routing, an external store and a signed token. Use when replicas is raised above 1, when a @Scheduled job suddenly runs N times, when a local cache disagrees between instances, when an in-memory rate-limit counter or idempotency map is the source of truth, when HttpSession holds anything a user would miss, when a service writes to java.io.tmpdir, or when a rolling deploy loses sessions. Does not cover pod replacement and drain (kubernetes-service-lifecycle), reaching a replica (load-balancing-and-routing), cache design (caching-strategies), fleet-singleton work (leader-election), state split by key (sharding-and-partitioning), pool arithmetic (connection-pool-sizing), or what replicas may observe (consistency-models).4---56# Stateless Service Design78## Purpose910Decide which state may stay instance-local and what replacement requires. “Stateless” does not11mean empty memory; it means request correctness/routing does not depend on a particular12instance's volatile history. Classify state by authority, durability, consistency scope,13reconstruction source/time and loss consequence. A local derivable cache can stay; a replicated14stateful actor/broker can also be correct, but it needs explicit ownership and recovery rather15than interchangeable stateless routing.1617The failure this prevents is the bug that cannot appear in any environment you have.18`replicas: 1` passes every test, because with one replica the process _is_ the shared store.19The counter, the idempotency map, the scheduled job and the local cache stay correct until20capacity is added, and then they are wrong quietly — a duplicate charge, a limit enforced at21N times its value, a job that emails everyone twice.2223## Workflow24251. **Apply loss, divergence and recovery tests.** If this JVM disappears, what correctness,26 accepted work, security decision, user journey or SLO changes? Can another instance rebuild27 from durable truth within RTO/RPO, and can copies diverge? Authoritative state may move to a28 shared store or become partitioned/replicated state with an explicit owner.292. **Inventory before you redesign.** Enumerate singleton bean fields, `static` collections,30 `HttpSession` attributes, caches, scheduler and executor queues, local files and31 long-lived connections; classify each as derivable, per-request or authoritative. The32 table and the grep shapes are in `references/state-inventory.md`.333. **Place session state deliberately.** Sticky routing, an external session store and a34 signed token are three different failure and revocation profiles, not three flavours of35 one idea. See `references/session-placement.md`.364. **Name the new authority and guarantee**—database row, durable queue/outbox, replicated37 partition or client token. Product labels do not decide semantics: Redis can be a cache or38 configured data store; verify eviction, persistence, replication, consistency, backup and39 failover before assigning authority.405. **Hunt singleton assumptions.** Plain application-context schedulers/startup hooks run per41 replica. Fleet-once work needs partitioning, a scheduler with documented coordination, or42 `leader-election`; TTL leases, session locks and durable job claims have different stale-43 owner/recovery semantics.446. **Exercise multiple replicas.** Route named steps deliberately to different instances45 (random balancing may miss the transition), overlap concurrent requests, then kill/restart46 one during work and deployment. A green run at47 `replicas: 1` does not establish cross-replica correctness. Fault injection belongs in an48 isolated or already authorized environment; successful cases cover only the paths exercised.497. **Check the next ceiling before celebrating.** Replication moves the bottleneck to what50 the replicas share. `replicas × maximumPoolSize` is a number the database has an opinion51 about; that arithmetic is `connection-pool-sizing`.5253## Decision block5455```text56Make the instance stateless and scale by replication when:57- every request's inputs are in the request plus a shared store, and the per-request58 working set is small enough to fetch inside the latency budget59- any instance may handle any key on the write path, with no ordering requirement the60 storage engine does not already provide61Keep the state in the process when:62- it is derivable from an authoritative source and its loss costs only latency (a cache)63- its lifetime is one request (a transaction, a request-scoped bean, a ScopedValue binding)64Prefer partitioning by key (sharding-and-partitioning) instead when:65- the per-key working set is too large or too hot to load per request, or the key needs66 single-writer ordering that shared storage would otherwise have to serialise67Prefer leader election (leader-election) instead when:68- the work must happen once per interval across the fleet rather than once per instance69 (election coordinates ownership; durable claims/idempotent effects are still needed where70 retries, failover or stale owners can repeat work)71```7273## Rules7475- Identify which copy owns the decision and can recover it. A field's size or lifetime does76 not establish authority; inspect loss, divergence and recovery together, including caches77 that temporarily influence security or correctness.78- Mutable `static` state on the request path needs a scope and authority check. Local metrics,79 protective limits and derivable caches can be valid; a local source of truth for a fleet-wide80 decision is unsafe. Inspect readers, writers, thread safety and divergence consequences.81- An uncoordinated in-process counter that gates a fleet-wide business decision enforces a82 separate budget per instance. Maximum aggregate allowance can approach N× under spread,83 though routing/skew changes observed behavior. Per-instance protective limits are valid when84 explicitly scoped (`rate-limiting-and-load-shedding`).85- Plain Spring `@Scheduled` runs once per application context. With one context per replica it86 runs N times unless an outer scheduler/claim/lease or idempotent work changes semantics.87- An in-memory idempotency map deduplicates only the requests that land on the same88 instance and retention window. Cross-replica/restart guarantees need an atomic shared claim89 and effect/recovery protocol, such as a durable unique-key record; `idempotency`90 owns the mechanics, this skill owns noticing that the map was never shared.91- A local cache can diverge after update/invalidation for its refresh/eviction/restart horizon;92 no TTL makes staleness unbounded unless explicit invalidation or replacement succeeds, not93 mathematically permanent. Cache design is94 `caching-strategies`; the multi-replica consequence is here.95- Trace local paths to actual mounts. Container writable layers can be lost on container96 replacement; `emptyDir` survives container restarts but ends with the Pod; persistent volumes97 have separate retention and access rules. Durability alone does not make a file reachable98 from another replica. A staged upload referenced by a later request needs that contract.99- `HttpSession` is in-process state by default. Anything in it a user would notice losing —100 cart contents, a multi-step form, an authorisation decision — needs a loss/staleness policy;101 it may be authoritative or reconstructible from another authority. Spring102 Session changes the store without changing the servlet API: a placement change, not a103 rewrite.104- **Sticky sessions give affinity, not a guarantee.** Affinity ends when the replica dies,105 when a rolling update drains it, when the client drops the cookie, or when the balancer's106 table is rebuilt. Each of those is user-visible if the state existed only there.107- A signed token moves claims to the client; signing provides integrity/authenticity, not108 confidentiality. Short expiry bounds token lifetime; revocation before expiry needs a109 verifier-enforced mechanism such as introspection, denylist/session version, or key/policy110 changes—each trades latency,111 blast radius and freshness. JWT is a format, not a session architecture.112- A WebSocket, SSE stream or long poll pins one user to one instance for the connection's113 lifetime. Pushing to that user from another replica needs a broker or a fan-out, and a114 replacing instance can terminate its streams. Clients need bounded reconnect and a115 cursor/replay or snapshot protocol when missed events affect correctness.116- Do not claim statelessness because a class has no fields. State hides in the framework117 too: session attributes, a `ThreadLocal` never cleared, a filter's cache, a library's118 static registry. Use the inventory and targeted failure evidence; code shape alone does not.119120## Stateful is not a defect121122Prefer explicit stateful ownership when locality, single-writer order or working-set cost123requires it. Then specify partition placement, replication/quorum, durable log/snapshot,124ownership epochs/fencing, failover/rebalance and backup/restore. Calling that service stateless125because an orchestrator can restart it erases its hardest contract.126127## Security and shutdown128129Inspect deployed Java, Spring/Session/Data Redis versions, storage mounts and routing before130changing placement. No upgrade is implied; ScopedValue is final in Java 25 and preview/incubator131in earlier supported releases. Missing recovery or durability evidence is unknown. Deliver the132state inventory, chosen authority/loss contract, checks performed and remaining failure cases.133134- Session/auth store failure must fail closed for protected actions. A separately authorized135 public/read-only degraded mode is possible; never reinterpret unknown authentication as136 authenticated.137- Stop admission, durably hand off accepted queues/uploads, drain connections and only then138 terminate. “No fields” does not prevent loss of in-flight accepted work.139- Bind token/session to issuer, audience, tenant and key version; protect against fixation,140 replay, key rotation overlap and cross-tenant cache keys.141142## References143144- [In-process state inventory](references/state-inventory.md) — every kind of in-process145 state with its classification, the failure it produces at `replicas > 1`, the grep or code146 shape that finds it, and where it moves. Read when auditing a service before scaling it147 out, or when a bug appears on some replicas and not others.148- [Where session state lives](references/session-placement.md) — sticky routing, an external149 store and a signed token compared on replica-death behaviour, deploy behaviour, per-request150 latency and revocation, with the Spring Session and token shapes and a decision block.151 Read when the service holds a session, or when a deploy logs users out.