Staff Engineer Guide
This guide is for engineers who must change Wow without weakening its boundaries. It describes the repository at main, not an aspirational architecture. Every concrete claim links to the code that establishes it. When the repository does not establish a property, this guide marks it as unknown.
Executive summary
Wow is a reactive CQRS and event-sourcing framework organized around aggregate-scoped command execution. The API module defines envelopes and contracts. The core module owns command dispatch, event sourcing, message processing, projections, sagas, snapshots, and runtime lifecycle. Spring modules adapt those mechanisms to dependency injection and application lifecycle. Infrastructure modules implement storage and transport contracts. WebFlux and OpenAPI share a runtime route catalog, while KSP produces metadata inputs at compile time. The most important consistency boundary is the append of a DomainEventStream to EventStore. Publication and downstream processing happen after that append. They are not one distributed transaction. Per-aggregate ordering is explicit; global ordering is not promised. Retries are selective, acknowledgements are explicit, and compensation is replay, not rollback.
WowRuntime is the sole lifecycle owner for registered runtime components.
It starts once, closes admission before draining, and uses a bounded shutdown deadline. Security adapters propagate identity-related headers and query tags. They do not prove authentication at the service boundary. The framework includes local, contract, integration, coverage, and JMH test layers. Those layers do not establish a production SLA or a universal throughput ceiling.
The single core insight
Wow turns a command into an immutable, versioned event stream inside one aggregate lane, persists that stream, and only then fans it out to independently owned consumers. Everything else protects or extends that sequence. The command envelope carries aggregate identity, ownership, tenant, request identity, and expected version. The aggregate root decides which event payloads to emit. The state aggregate sources those events. The event store performs the durable append. The event and state-event buses drive projections, sagas, and snapshots afterward.
WowRuntime controls whether these processors may accept work.
The following Python-like pseudocode is explanatory. It deliberately makes the non-transactional fan-out visible.
async def execute(command):
lane = lane_for(command.aggregate_id)
async with lane.serialized():
state = await snapshots.load(command.aggregate_id) or new_state()
async for stream in events.load_from(state.next_version):
state.source(stream)
emitted = await aggregate.decide(command, state)
state.source(emitted)
# The durable command consistency boundary.
await events.append(emitted)
# Downstream effects are separate reactive operations.
await domain_event_bus.send(emitted)
await state_event_bus.send_best_effort(emitted.with_state(state))The real implementation sources the in-memory state before appending and marks the command aggregate expired if persistence fails. The append contract detects version conflicts and duplicate request IDs. The domain-event send is a filter after aggregate processing. The state-event send is later still and resumes after logging a send error. Sources: command envelope, aggregate execution, event-store contract, post-append publication, best-effort state event.
System architecture
The architecture is layered by responsibility, not by deployment topology. Applications may combine modules in one process or place distributed buses and stores between processes. The repository does not define one mandatory production topology.
Ownership table
| Area | Owner | Delegates to | Boundary evidence |
|---|---|---|---|
| Public command, event, naming, and modeling contracts | wow-api | Nothing below core | minimal API dependencies |
| Command and event runtime | wow-core | Store and bus interfaces | core dependencies |
| Spring integration | wow-spring | Core services and Spring container | module dependencies |
| Optional Spring Boot composition | wow-spring-boot-starter | Feature variants | capabilities |
| HTTP entry and route materialization | wow-webflux | RouterSpecs and handlers | module dependencies |
| Route contracts and OpenAPI rendering | wow-openapi | Metadata, contributors, schema context | RouterSpecs |
| Kafka transport | wow-kafka | Reactor Kafka | module boundary |
| MongoDB persistence | wow-mongo | Mongo driver | module boundary |
| Redis persistence | wow-redis | Lettuce and Redis scripting | module boundary |
| Elasticsearch persistence and querying | wow-elasticsearch | Elasticsearch client | module boundary |
| CoSec integration | wow-cosec | WebFlux request context | adapter dependency |
| Domain test DSL | test/wow-test | Core and JUnit | test module |
| Backend contracts | test/wow-tck | Store and dispatcher interfaces | TCK module |
Dependency direction
Infrastructure is replaceable because core depends on interfaces. The starter composes optional capabilities without moving persistence or transport behavior into core.
Load-bearing contracts
CommandMessage
CommandMessage is the runtime envelope around a business command body.
It carries aggregateId, owner and space context, command ID, request ID, and copy semantics. It also carries expected version and flags governing aggregate creation, voiding, and creation allowance. These fields are framework control data, not domain state. Source: CommandMessage.
DomainEvent
DomainEvent wraps a business event payload with aggregate identity, sequence, revision, and stream position.
The business event can remain a plain Kotlin class or object. The example OrderCreated is a data class with business fields only. Sources: DomainEvent, OrderCreated.
DomainEventStream
One event stream represents the events produced by one command execution. Its contract says the command ID has a one-to-one relation with the stream. The concrete stream is non-empty and derives aggregate and version metadata from its first event. Source: DomainEventStream.
EventStore
EventStore owns append, request lookup, version lookup, and stream loading contracts.
The append contract names version conflict, duplicate aggregate ID, and duplicate request ID outcomes. It does not define a transaction spanning event publication or projection updates. Source: EventStore.
SnapshotStore
SnapshotStore loads and saves state checkpoints.
Its save rule is monotonic: a lower version must not replace a higher version atomically. The interface has no delete or retention operation. Source: SnapshotStore.
MessageBus
MessageBus separates sending from receiving.
Receiver readiness is part of the contract and lifecycle belongs to WowRuntime. Local sendIfSubscribed is conservative until processing admission succeeds. Source: MessageBus.
RuntimeComponent
Construction must be inert.
prepare, start, quiesce, graceful stop, and force stop are distinct phases.
The contract deliberately avoids AutoCloseable so arbitrary callers cannot own shutdown. Source: RuntimeComponent.
Domain model and invariants
The framework separates payload classes, framework envelopes, command behavior, and event-sourced state. That separation supports event-sourced design, but the framework does not prevent a command handler from mutating the state object directly. Enforce the convention in the domain model: keep state setters private and have command handlers return events that sourcing handlers apply. Source: command-root construction, encapsulated cart state.
Framework invariants
| Entity | Invariant | Enforced By | Consequence | Source |
|---|---|---|---|---|
CommandMessage | Commands are routed by named aggregate and aggregate ID | CommandMessage.aggregateId | Identity is part of every command envelope. | Source |
CommandAggregate | Expected version must match when supplied | SimpleCommandAggregate | A stale writer fails before domain invocation. | Source |
CommandAggregate | A supplied owner or space must match initialized aggregate state | SimpleCommandAggregate | A non-blank mismatching value is rejected before command handling; blank values skip this comparison. This is a consistency check, not complete authorization. | Source |
CommandAggregate | Deleted aggregates reject ordinary commands | SimpleCommandAggregate | Deletion is a domain-access guard. | Source |
StateAggregate | State is sourced before persistence | SimpleCommandAggregate | In-memory state reflects emitted events during processing. | Source |
CommandAggregate | Persistence failure expires the aggregate instance | SimpleCommandAggregate error hook | A failed in-memory instance is not reused as authoritative state. | Source |
Snapshot | Snapshot versions do not move backward | SnapshotStore | Concurrent older saves cannot replace newer state. | Source |
| Aggregate group | One lane processes a group sequentially | AggregateDispatcher | Ordering is per group, not global. | Source |
Example order aggregate
The example Order demonstrates the intended split.
Order receives commands and returns events.
OrderState applies events and owns mutable state with private setters.
CreateOrder validates input and OrderCreated is an immutable event payload.
Payment emits one or two ordered events depending on the amount. The state transition rules are explicit: address changes only while created, shipping only while paid, and receipt only while shipped. Sources: Order handlers, OrderState, CreateOrder.
aggregateVersion is nullable; omitting it disables the optimistic-version precondition. Event revision is a semantic-version string and defaults to 0.0.1.
Command lifecycle
Entry
CommandHandlerFunction extracts body, path variables, and headers, then delegates to CommandHandler.
CommandHandler builds the command message and selects SSE or ordinary wait behavior.
Sources: HTTP handler function, command handler.
Gateway
DefaultCommandGateway validates the message and checks request identity before sending.
Waiting uses a handle with an absolute timeout rather than extending the deadline at every stage. Sources: validation and request check, wait deadline.
Dispatch
CommandDispatcher receives command exchanges from the configured CommandBus—local, distributed, or their merged local-first view—and resolves aggregate metadata.
It creates an aggregate-specific dispatcher and scheduler. The dispatcher groups work so one aggregate lane remains sequential. Sources: dispatcher creation, aggregate dispatcher.
Load and decide
The repository loads a snapshot or creates a fresh state aggregate. It then replays event streams from the next expected version. The command aggregate checks version and deletion state before invoking the handler. For an initialized aggregate, it also compares owner or space when the corresponding message value is non-blank. Sources: snapshot plus replay, preconditions.
Persist and publish
The aggregate sources emitted events and appends the stream. Only after aggregate processing completes does the domain-event filter send the stream. The state-event filter follows the domain-event filter. Its send failure is logged and resumed. Sources: append, domain send, state send.
Command aggregate state
Event, projection, saga, and snapshot lifecycles
Domain-event dispatch
The domain dispatcher owns both domain-event and state-event child dispatchers. Function kind selects the relevant child. Within one stream, events are handled with concatMap. Normal event processor return values are discarded after completion. Sources: composite dispatcher, per-stream handling, function return discarded.
Projections
ProjectionDispatcher subscribes to both domain-event and state-event buses.
It uses the event function filter, so a projection's publisher represents completion, not new domain events. Sources: ProjectionDispatcher, ProjectionFunctionFilter.
Stateless sagas
A stateless saga is the special path that converts handler results into commands. Generated request IDs derive from the source event ID and result index. Tenant, space, and upstream headers propagate to the new command. This is command choreography. It is not a distributed transaction and it does not automatically undo prior side effects. Source: StatelessSagaFunction.
Snapshots
Snapshots are derived checkpoints consumed from state events.
The Starter defaults to the ALL snapshot strategy. When VERSION_OFFSET is selected, VersionOffsetSnapshotStrategy defaults to an offset of five versions.
It compares the stored snapshot version and saves a newer SimpleSnapshot when required. Snapshot saving is outside the event-store append transaction. Sources: Starter snapshot defaults, strategy contract, version-offset strategy, snapshot filter.
Lifecycle comparison
| Artifact | Created by | Durable boundary | Consumer | Failure interpretation |
|---|---|---|---|---|
| Command message | Gateway or bus client | Bus-specific | Command dispatcher | Validation or transport failure |
| Domain event stream | Command aggregate | EventStore.append | Domain-event bus | Version, duplicate, or store failure |
| Domain event delivery | Post-append filter | Bus-specific | Event processor, projection, saga | Retry, handler policy, then acknowledgement |
| State event | Post-domain-event filter | Bus-specific | Projection and snapshot dispatchers | Immediate send error is logged and resumed |
| Snapshot | Snapshot strategy | SnapshotStore.save | Aggregate repository | Derived checkpoint may lag event store |
| Saga command | Stateless saga result mapper | Command bus and later event append | Another aggregate | No implicit rollback of the source event |
Sources: send filters, retry filter, ack semantics, saga mapping.
Runtime lifecycle
WowRuntime is a one-shot lifecycle coordinator.
Its states are NEW, STARTING, RUNNING, STOPPING, FORCE_STOPPING, and STOPPED. Preparation is a barrier before component start. Unexpected component failure closes admission and initiates shutdown. Graceful shutdown has one owner and one global deadline. The sequence closes global admission, quiesces components, drains work, and stops components in reverse order. Timeout or graceful-stop failure escalates to force stop. Startup cleanup is a lifecycle rollback only. It is not rollback of domain events or external side effects. Sources: states and topology, start and startup cleanup, shutdown ownership, shutdown sequence.
Component ordering
Components are registered in ordered, identity-distinct slots. Prepare and start run in registration order. Graceful and force stop run in reverse order. The first failure is retained while later cleanup still runs. Source: RuntimeComponentGroup.
Spring bridge
The Spring lifecycle bridge starts Wow early enough for ingress to see a ready runtime. It stops after ingress drains and closes the application context on unexpected runtime termination. Default shutdown timeout is 60 seconds and quiet period is one second. Sources: WowRuntimeLifecycle, WowProperties.
Storage architecture
Storage is selected per aggregate through registries and routing decorators. The router itself owns lifecycle and delegates each operation to the selected backend. An aggregate-specific mapping wins over the default store. Sources: RoutingEventStore, event registry, snapshot router, snapshot registry.
Backend comparison
| Backend | Event store | Snapshot store | Important boundary | Source |
|---|---|---|---|---|
| In-memory | Yes | Yes | Development and test semantics; durability is process-local | InMemoryEventStore, InMemorySnapshotStore |
| MongoDB | Yes | Yes | Sorted event loading; direct or optional batched append | MongoEventStore, MongoSnapshotStore |
| Redis | Yes | Yes | Lua append checks conflicts; event-time loading is unsupported | RedisEventStore, RedisSnapshotStore |
| Elasticsearch | Yes | Yes | Refresh and optional batching affect visibility and latency | ElasticsearchEventStore, ElasticsearchSnapshotStore |
Batching
MongoDB and Elasticsearch batching is opt-in. The default options disable batching because partial batches add up to maxDelay latency. Default option values include a maximum batch size of 128, pending capacity of 4096, one lane, and one millisecond delay. These are configuration defaults, not measured optimal values for every workload. Sources: Mongo event options, Elasticsearch event options. Pending queues are bounded and can reject admission with a typed overload error. That is intentional backpressure, not silent buffering. Source: Mongo batch appender.
Messaging architecture
Per-aggregate ordering
AggregateDispatcher maps a message to a group key.
Each group uses publishOn followed by concatMap for sequential handling. Different groups can run in parallel. The default lane count is 64 * available processors, with a system-property override. This is not a global ordering guarantee. Sources: group processing, parallelism.
Local-first behavior
Local-first sends a local delivery copy and a distributed copy. The distributed copy is marked locally handled only after local admission succeeds. If local delivery errors, the distributed path remains eligible. Filtered distributed copies are acknowledged. This is an admission-aware optimization. It is not proof of cluster-wide exactly-once processing. Source: LocalFirstMessageBus.
Kafka
Kafka send completes from the sender result. Receive uses a consumer group, retries the receive stream, and decodes records sequentially. The Kafka key is the aggregate ID string. The topic converter supplies aggregate and function routing context. Sources: send and receive, subscription, key and serialization. Default Kafka receiver policy uses prefetch one, maximum deferred acknowledgement one, three retry attempts, and a ten-second retry delay. These values are configuration defaults, not throughput guarantees. Source: KafkaReceiverPolicy.
Acknowledgement semantics
finallyAck acknowledges after success.
On error it acknowledges first and then rethrows the error. Therefore exhausted handler failure does not by itself imply broker redelivery. Retry and compensation policy must be understood before relying on replay behavior. Source: ExchangeAck.
Failure handling
The default retry filter retries at most three times with two-second backoff. It retries only exceptions classified as recoverable. It is ordered before aggregate, event-function, and snapshot processing filters. The default event-processor error handler logs and resumes after the chain policy completes. Sources: RetryableFilter, event auto-configuration. Compensation reloads persisted events, marks them with a compensation target, and resends them. State-event compensation reconstructs state through event sourcing before resending. Neither path reverses the original event-store append. Sources: domain compensation, state compensation.
Failure-mode table
| Failure | Immediate behavior | Durable truth | Staff-engineer action |
|---|---|---|---|
| Expected-version mismatch | Reject before handler invocation | Existing event stream | Treat as optimistic concurrency conflict |
| Duplicate request ID | Event-store contract rejects duplicate | First accepted stream | Keep request IDs stable across client retry |
| Event-store append failure | Aggregate becomes expired | Backend decides whether append committed | Reload before reusing state; inspect backend result |
| Domain-event send failure | Command stream may already be durable | Event store remains source of truth | Use retry or explicit compensation based on classification |
| State-event send failure | Error is logged and resumed | Event stream remains durable | Monitor lag and use state-event compensation when required |
| Projection handler failure | Retry selective, then ack/error policy | Projection may lag | Make handler idempotent and define replay runbook |
| Saga command failure | Source event remains committed | No automatic rollback | Model compensating business commands explicitly |
| Runtime startup failure | Started components are cleaned up | No domain rollback implied | Inspect first failure and cleanup failure |
| Graceful shutdown timeout | Escalate to force stop | In-flight outcome may be uncertain | Reconcile by request ID and event-store state |
Sources: append errors, aggregate expiration, runtime shutdown.
Metadata, generated code, routes, and OpenAPI
These are related pipelines, not one generation step.
Compile-time KSP metadata
MetadataSymbolProcessor scans bounded contexts and aggregate roots.
It merges the result and writes the metadata resource as JSON.
AggregatesMetadataResolver separately generates Kotlin accessors that call aggregateMetadata<Command, State>(), which invokes the runtime aggregate metadata parser.
These are two runtime inputs, not one discovery chain: MetadataSearcher loads the JSON resource, while generated accessors invoke AggregateMetadataParser through aggregateMetadata(). Sources: metadata resource generation, aggregate accessor generation, resource search, runtime parser.
Runtime route catalog
RouterSpecs orders route contributors.
It reads runtime MetadataSearcher entries, filters disabled aggregate routes, and builds a validated RouteCatalog. The catalog rejects duplicate route keys and path-variable mismatches. Sources: route collection, catalog validation.
Runtime WebFlux materialization
RouterFunctionBuilder iterates the route catalog.
It materializes each contract into a predicate and handler function. Spring Boot creates this router from RouterSpecs and the handler registrar. Sources: RouterFunctionBuilder, WebFlux auto-configuration.
Runtime OpenAPI rendering
The same catalog is rendered into OpenAPI 3.1 paths and components. Springdoc customization merges that generated catalog into the application OpenAPI object. Sources: OpenAPI rendering, OpenAPI auto-configuration.
Reflection boundary
Do not describe Wow as a zero-reflection framework. Core declares Kotlin reflection as an API dependency. Metadata parser documentation explicitly includes reflective analysis. The test DSL also reflects generic type arguments. KSP removes some discovery and registration boilerplate, but it does not prove zero runtime reflection. Sources: core reflection dependency, metadata parser contract, AggregateSpec reflection.
Security and trust boundaries
Request context
WebFlux extracts tenant, owner, space, aggregate ID, and local-first hints from paths and headers. Extraction is not authentication. The deployment must decide which headers are accepted from an untrusted client and which are overwritten by a trusted edge. Source: AggregateRequest.
CoSec adapter
The CoSec extractor copies request ID and space ID headers into the command builder. Other CoSec adapters propagate app and device IDs. The module boundary depends on WebFlux and does not itself establish an authenticator. Sources: builder extractor, message propagation, module dependency.
Aggregate authorization preconditions
For initialized aggregates, command processing checks owner and space equality only when the corresponding message value is non-blank. Read-side owner preconditions can reject access to an owner aggregate. Route metadata controls whether owner paths are never, always, or aggregate-ID based. Sources: command checks, owner precondition, route ownership.
These conditional checks protect aggregate context when it is supplied; they do not authenticate the caller or replace endpoint authorization.
Query ABAC
AbacQueryFilter converts principal tags into query conditions.
Principal tag resolution is abstract and must be supplied by an integration. An empty tag set resolves to Condition.all(). Therefore the presence of this filter alone does not prove an authenticated or restricted query. Source: AbacQueryFilter.
Security checklist
- Terminate external authentication before trusting Wow identity headers.
- Strip client-supplied internal headers at the edge.
- Bind tenant, owner, and space to the authenticated principal.
- Provide a concrete principal-tag resolver for ABAC.
- Test the empty-tag behavior explicitly.
- Treat the local-first header as an internal routing hint.
- Verify compensation endpoints have operator authorization.
- Verify metadata and BI-script endpoints match exposure policy.
- Audit generated OpenAPI before publishing it externally.
- Keep store credentials and signing material outside the repository.
The code establishes the extraction and filtering points above. The concrete production identity provider, edge policy, and secret store are unknown from this repository.
Performance model
Structural hot path
The write path includes request decoding, validation, request-ID checks, bus admission, lane scheduling, snapshot load, tail replay, domain invocation, event serialization, store append, publication, and optional wait coordination. The dominant cost depends on workload and deployment. The repository does not prove one universal bottleneck.
Explicit bounds and knobs
| Knob | Code default | What it bounds | What it does not prove |
|---|---|---|---|
| Dispatcher lanes | 64 * processors | In-process grouping parallelism | Optimal CPU or store concurrency |
| Kafka prefetch | 1 | Receiver demand | End-to-end throughput |
| Kafka deferred ack | 1 | Outstanding deferred acknowledgement | Delivery guarantee |
| Kafka retries | 3, 10s delay | Receive-stream retry policy | Handler replay after final ack |
| Batch max size | 128 | One optional storage batch | Best batch size for a workload |
| Batch max pending | 4096 | Pending queue capacity | Safe memory or latency at saturation |
| Batch lanes | 1 | Coordinator lane count | Universal optimal ordering strategy |
| Batch max delay | 1ms | Partial-batch wait | End-to-end latency |
| Runtime timeout | 60s | Default shutdown deadline | Business-operation deadline |
Sources: message parallelism, Kafka receiver policy, Mongo batch options, runtime defaults.
Benchmark evidence
The benchmark module includes component, end-to-end, WebFlux, MongoDB, Redis, and Elasticsearch fixtures. It uses JMH and depends on example, test, mock, and infrastructure modules. Sources: benchmark dependencies, JMH version. The simulated-I/O benchmark studies I/O latency and scheduler handoff. The batch E2E benchmark normalizes measurements per command. The concurrency benchmark says repeated-key ordering belongs to functional tests rather than its throughput measurement. Sources: simulated I/O benchmark, batch E2E benchmark, coordinator benchmark scope.
README stress sample
The README reports one two-minute stress test of the example application. It lists measured average and peak TPS for particular operations and wait plans. Those numbers are a historical sample under the linked deployment setup. They are not an SLA, a capacity plan, or a component performance ceiling. Source: README sample.
Performance decision rule
Use a reproducible workload. Pin the code revision and environment. Measure store, broker, CPU, allocation, and scheduler behavior together. Separate component screening from end-to-end confirmation. Retest ordering and overload behavior when changing concurrency. Do not change a default from one quick benchmark. Do not transfer EventStore results to SnapshotStore without measurement. Production capacity and tail-latency targets are unknown until a deployment-specific experiment supplies them.
Testing strategy
Layers
| Layer | Purpose | Evidence |
|---|---|---|
| Domain spec | Given/when/expect behavior | AggregateSpec |
| Saga spec | Isolated emitted-command expectations | SagaSpec |
| Event-store TCK | Append, load, conflict, duplicate, concurrency | EventStoreSpec |
| Snapshot-store TCK | Load, monotonic save, concurrency | SnapshotStoreSpec |
| Backend contract implementations | Run TCK against real adapters | Mongo event test, Redis event test, Elasticsearch event test |
| Integration CI | Services plus aggregate integration tasks | workflow |
| Static analysis | Detekt | workflow |
| Coverage | Jacoco is enabled for library projects; thresholds are configured by individual modules where required | root Jacoco wiring, example 80% rule |
| Benchmark | JMH regression and diagnosis | benchmark module |
Domain test style
The DSL exposes dynamic JUnit tests around Given, When, and Expect phases. Generic command aggregate type discovery in AggregateSpec uses reflection. Example domain modules can enforce an 80 percent Jacoco floor. Sources: AggregateSpec factory, example coverage.
Change-to-test map
| Change | Minimum focused verification | Broader gate |
|---|---|---|
| Command validation or handler | Aggregate spec for success and rejection | Domain module check |
| Event sourcing rule | Replay from full history and snapshot tail | Store TCK plus integration test |
| Event-store adapter | Conflict, duplicate request, ordering, concurrency | Adapter module check and integration workflow |
| Snapshot adapter | Monotonic concurrent save | Snapshot TCK and adapter check |
| Dispatcher concurrency | Same-key order, cross-key parallelism, quiesce | Core tests and benchmark diagnosis |
| Runtime lifecycle | Prepare barrier, reverse cleanup, timeout, cancellation | :wow-core:test |
| Route contributor | Catalog validation and route snapshot | OpenAPI and WebFlux tests |
| Metadata KSP | Generated resource and accessor golden output | Compiler module check |
| Security filter | Authenticated, unauthenticated, empty-tag, forged-header cases | WebFlux integration test |
| Performance default | Multiple-fork component and E2E comparison | Deployment-representative load test |
Green tests establish only their fixtures and assertions. They do not prove real-provider cancellation, production authorization, migration safety, or a deployment SLA unless those conditions are in the test.
Architecture decisions
The repository does not contain a cited ADR that records the historical alternatives or original motivation for these mechanisms. Not declared is therefore deliberate: each rationale below is a present-day architectural interpretation of the cited behavior, not proof of historical design intent.
| Decision | Alternatives Considered | Rationale | Source |
|---|---|---|---|
| Separate business payloads from framework envelopes | Not declared | Current envelopes keep routing, identity, ownership, and version controls outside the business payload; this is an interpretation of the present contract. | CommandMessage |
| Persist before publishing domain events | Not declared | Current filter order makes the event-store append complete before downstream publication, leaving consumers able to lag or replay. | send filter order |
| Treat snapshots as derived checkpoints | Not declared | The current strategy saves sourced state after event processing and does not replace event history. | snapshot strategy |
| Serialize work by aggregate-derived group | Not declared | Current grouped concatMap processing protects same-group order while allowing different groups to progress independently. | AggregateDispatcher |
| Use admission-aware local-first delivery | Not declared | The implementation attempts local delivery while retaining a marked distributed copy, trading broker avoidance for more complex copy and acknowledgement semantics. | LocalFirstMessageBus |
| Give one runtime exclusive lifecycle ownership | Not declared | The current contract separates prepare, start, quiesce, graceful stop, and force stop so readiness and cleanup have one coordinator. | RuntimeComponent |
| Route stores per aggregate | Not declared | Current registries allow aggregate-specific storage selection while preserving a default backend. | store registries |
| Compose adapters through starter capabilities | Not declared | Feature variants keep infrastructure modules selectable, while variant resolution becomes part of release compatibility. | starter capabilities |
| Share one validated route catalog | Not declared | The current catalog is consumed by both route and OpenAPI materialization, reducing contract drift between those outputs. | RouterSpecs |
| Make compensation an explicit replay operation | Not declared | The current compensator reloads persisted events and resends them toward a target; it does not reverse the original append. | DomainEventCompensator |
Dependency rationale
The catalog pins Kotlin 2.4.10, KSP 2.3.11, Spring Boot 4.1.1, JUnit 6.1.3, Testcontainers 2.0.5, and JMH 1.37. Source: version catalog.
No cited ADR or migration record identifies what these dependencies replaced. The What It Replaced column therefore remains Not declared instead of inventing history.
| Dependency | Purpose | What It Replaced | Source |
|---|---|---|---|
| Kotlin and KSP | Kotlin implements the framework and KSP generates metadata resources and typed accessors at compile time. | Not declared | version catalog, compiler dependencies |
| Spring Boot | Supplies auto-configuration, lifecycle integration, WebFlux composition, and feature variants. | Not declared | starter features |
| Reactor | Provides the non-blocking publisher model used by command, event, retry, ordering, and drain paths. | Not declared | core dependencies |
| Jackson | Serializes command, event, state, and metadata representations. | Not declared | core dependencies, message serializer |
| Reactor Kafka | Implements the distributed Kafka message-bus adapter. | Not declared | Kafka module |
| MongoDB reactive driver | Implements MongoDB event, snapshot, and query persistence. | Not declared | MongoDB module |
| Spring Data Redis and Lettuce | Implement Redis event and snapshot persistence plus Redis transport integration. | Not declared | Redis module |
| Spring Data Elasticsearch | Implements Elasticsearch event, snapshot, and query adapters. | Not declared | Elasticsearch module |
| Swagger/OpenAPI libraries | Model and render the runtime route catalog as an OpenAPI contract. | Not declared | OpenAPI module |
| JUnit and Testcontainers | Provide dynamic domain tests, backend TCK fixtures, and external-service integration tests. | Not declared | TCK dependencies |
Known technical debt
The repository does not label these gaps as debt in a cited ADR or issue. The qualitative risk levels are review priorities derived from current impact, not maintainer commitments.
| Issue | Risk Level | Affected Files | Source |
|---|---|---|---|
| Redis cannot implement the public event-time loading capability, so time-range replay is unavailable on that backend. | Medium | EventStore.kt, RedisEventStore.kt | contract, Redis implementation |
| Snapshot deletion and retention are absent from the public store contract, leaving lifecycle policy to backend operations or an additional application contract. | Medium | SnapshotStore.kt, selected snapshot backend and deployment policy | SnapshotStore |
Explicit framework boundaries and intentional constraints
These behaviors are code-confirmed boundaries. They should not be called technical debt without an ADR, issue, or maintainer decision that establishes remediation intent.
| Constraint | Engineering implication | Source |
|---|---|---|
| State-event send errors are logged and resumed at the immediate filter boundary. | Snapshot and state-event consumers may lag; concrete bus durability and replay policy must close the operational gap. | SendStateEventFilter |
| Authentication and principal-tag resolution are integration-owned. | Header extraction and ABAC hooks alone do not establish authenticated or restricted access. | CoSec extraction, ABAC empty tags |
| Ordinary event-processor return values have no publication semantics. | Use stateless saga mapping when event results must become commands. | event function filter, saga mapper |
WowRuntime and its Spring bridge are one-shot. | Embedding code must replace the runtime rather than restart a stopped instance. | one-shot start, Spring lifecycle states |
| KSP does not remove runtime aggregate reflection. | AOT, startup, or reflection-reduction work must measure the actual parser and invocation path. | generated accessor, runtime parser |
Unknowns that require deployment evidence
- The production authentication provider is unknown.
- The trusted proxy and header sanitation policy are unknown.
- The production event-store and snapshot-store selection per aggregate are unknown.
- The broker replication and retention policy are unknown.
- The disaster-recovery RPO and RTO are unknown.
- The projection replay runbook is unknown.
- The compensation endpoint authorization policy is unknown.
- The acceptable state-event lag is unknown.
- The production command latency SLO is unknown.
- The safe maximum concurrency for any deployment is unknown.
- The capacity ceiling of each storage backend is unknown.
- The migration policy for event payload schema changes is not established by the cited contracts.
- The retention policy for event streams and snapshots is unknown.
- The operational response to a partially completed force stop is unknown.
- Whether clients preserve request IDs across network retries is unknown.
These are not framework defects by themselves. They are inputs a production design must supply.
Staff engineer change protocol
Before design
- Name the aggregate, bounded context, and module that owns the behavior.
- Identify the durable truth: event store, snapshot, projection, or external system.
- Trace the envelope fields and metadata used for routing.
- Identify the runtime component that owns admission and shutdown.
- State whether the change affects one aggregate lane or cross-aggregate coordination.
- List the exact retry, acknowledgement, and compensation behavior.
- Identify trusted and untrusted headers.
- Decide whether KSP output, runtime route catalog, or both change.
- Define backward compatibility for persisted events and public routes.
- Write the failure-mode test before implementation when behavior changes.
During implementation
- Keep public contracts in
wow-api. - Keep runtime behavior in
wow-core. - Keep Spring wiring in
wow-spring*. - Keep transport and storage details in adapter modules.
- Preserve non-blocking Reactor paths.
- Preserve per-aggregate ordering.
- Do not widen acknowledgement semantics accidentally.
- Do not hide send failures without an explicit replay path.
- Do not hand-edit generated outputs as the primary fix.
- Keep route and OpenAPI materialization driven by the same catalog.
Before merge
- Run the narrowest module test first.
- Run the relevant store or dispatcher TCK.
- Run static analysis for touched Kotlin.
- Render and compare generated OpenAPI when routes change.
- Inspect generated metadata when annotations change.
- Test startup, graceful stop, and force stop when lifecycle changes.
- Test same-key ordering and cross-key parallelism when concurrency changes.
- Test recoverable and unrecoverable errors separately.
- Verify compensation is idempotent for affected processors.
- Record remaining unknown deployment assumptions.
Recommended reading order
- Start with
CommandMessageto understand the control envelope. - Read
DomainEventand separate payload from metadata. - Read
DomainEventStreamfor the command-to-stream relation. - Read
SimpleCommandAggregatefor the consistency boundary. - Read
EventStorefor persistence contracts. - Read
EventSourcingStateAggregateRepositoryfor snapshot-plus-tail replay. - Read the two publication filters for post-append boundaries.
- Read
AggregateDispatcherfor ordering. - Read
LocalFirstMessageBusfor admission-aware delivery. - Read
ExchangeAckbefore changing failure policy. - Read
RetryableFilterfor retry classification. - Read
DomainEventCompensatorfor replay semantics. - Read
StatelessSagaFunctionfor cross-aggregate choreography. - Read
VersionOffsetSnapshotStrategyfor snapshot timing. - Read
RuntimeComponentbefore lifecycle code. - Read
WowRuntimefor startup ownership. - Continue through shutdown ownership.
- Read
RuntimeComponentGroupfor ordering and cleanup. - Read
MetadataSymbolProcessorfor compile-time metadata. - Read
RouterSpecsfor runtime route and OpenAPI assembly. - Read
RouterFunctionBuilderfor HTTP materialization. - Read the order example only after the framework boundaries are clear.
Review heuristics
Reject a change that treats a snapshot as the source of truth. Reject a change that publishes before the event-store append without a new, explicit consistency model. Reject a change that introduces blocking I/O in command, event, projection, saga, or store reactive paths. Reject a claim of exactly-once processing without broker, acknowledgement, handler idempotency, and replay evidence. Reject a claim of automatic rollback when the code only provides retry or compensation replay. Reject a claim of zero reflection while reflective dependencies and parsers remain. Reject a performance default change backed only by the README sample or one quick JMH run. Reject an authorization claim based only on header extraction. Require an explicit migration path for persisted event schema changes. Require lifecycle tests for new runtime components. Require overload tests for new queues or batch coordinators. Require route-catalog and OpenAPI checks for route metadata changes.
Glossary
Aggregate lane — the sequential processing group derived from an aggregate identifier. Command envelope — CommandMessage plus routing, identity, ownership, and version control data. Domain event payload — the application-defined immutable object describing a fact. Domain event stream — the non-empty ordered events emitted by one command execution. Event sourcing — rebuilding state by applying persisted event streams after an optional snapshot. State event — a domain event stream decorated with the sourced aggregate state. Snapshot — a derived state checkpoint used to reduce replay work. Projection — an event consumer that updates a read-oriented model. Stateless saga — an event function whose results are converted into new commands. Compensation — explicit replay of persisted domain or reconstructed state events toward a target function. Local-first — attempt local admission while retaining a distributed delivery path. Quiesce — stop accepting new work while allowing admitted work to drain. Force stop — best-effort shutdown after graceful completion is no longer possible. Route catalog — validated runtime contracts shared by WebFlux route and OpenAPI materialization. Generated metadata — KSP produces a JSON resource loaded by MetadataSearcher and separate accessors that invoke the runtime aggregate metadata parser.
Final mental model
Start with one aggregate and one command. Follow its envelope to one serialized lane. Rebuild state from snapshot plus event tail. Let the aggregate emit events rather than mutate storage. Append one event stream as the durable command result. Treat every later bus, projection, saga, and snapshot effect as a separately owned asynchronous boundary. Let WowRuntime decide when those owners can accept and finish work. Use retries for classified transient failures. Use explicit compensation replay for persisted events that require downstream reprocessing. Use evidence, not labels, for security, delivery, and performance guarantees.