Add Wow to an Existing Spring Boot Project
Adopt Wow as one reversible vertical slice first:
KSP metadata → Spring auto-configuration → generated HTTP route
→ command aggregate → event store / snapshot → versioned state readDo not migrate every write path or add production infrastructure in the same change. The first milestone is one aggregate running on explicit in-memory adapters with its old write path still available for rollback.
Version Baseline
The current Wow 9.0.18 source declares:
| Component | Version |
|---|---|
| JDK | 17+ |
| Wow | 9.0.18 |
| Spring Boot | 4.1.1 |
| Kotlin | 2.4.20 |
| KSP | 2.3.11 |
| CosId | 3.2.1 |
| Springdoc | 3.1.1 |
Treat these as a compatibility train, not independent suggestions. For another Wow release, inspect that tag's gradle/libs.versions.toml, release notes, and persisted-event requirements before changing the application.
1. Define the Adoption Boundary
Choose one use case with a clear aggregate ID and business invariant. Before editing, record:
- the existing HTTP/write entry point and current source of truth;
- the Wow command route that will be introduced;
- whether the first run is local-only or receives real traffic;
- the state comparison or reconciliation needed before cutover;
- the last point where removing Wow dependencies is a sufficient rollback.
For a new route with no production traffic, rollback is simple. After commands have appended events, removing the runtime is not data rollback; see Rollback Boundary.
2. Select Dependencies and Capabilities
Every module containing Wow annotations applies KSP and depends on wow-compiler. In a multi-module service, a minimal responsibility split is:
| Module | Responsibilities | Required Wow pieces |
|---|---|---|
api | bounded context, commands, events, @CommandRoute | wow-api, KSP, wow-compiler |
domain | @AggregateRoot, @OnCommand, @OnSourcing, domain specs | wow-core or wow-spring, KSP, wow-compiler, wow-test |
server | Spring Boot host and runtime adapters | base Starter plus selected capabilities |
Align the platform and compiler in the annotated modules:
dependencies {
implementation(platform("me.ahoo.wow:wow-bom:9.0.18"))
ksp(platform("me.ahoo.wow:wow-bom:9.0.18"))
implementation("me.ahoo.wow:wow-api") // api module
ksp("me.ahoo.wow:wow-compiler")
testImplementation("me.ahoo.wow:wow-test") // domain module
}Request the base Starter and the webflux-support feature in the server module:
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:4.1.1"))
implementation(platform("me.ahoo.wow:wow-bom:9.0.18"))
implementation("me.ahoo.wow:wow-spring-boot-starter")
implementation("me.ahoo.wow:wow-spring-boot-starter") {
capabilities {
requireCapability("me.ahoo.wow:webflux-support")
}
}
implementation("me.ahoo.cosid:cosid-spring-boot-starter:3.2.1")
}The base Starter supplies core Spring wiring. webflux-support selects wow-webflux, which materializes command and state routes from RouterSpecs. It is a Gradle Feature Capability, not a property flag.
Add only a backend required by the target runtime:
| Requirement | Capability |
|---|---|
| Local single-process proof | none; use in-memory configuration |
| Distributed command/event/state buses | kafka-support |
| MongoDB event/snapshot storage and queries | mongo-support |
| Redis event/snapshot storage or bus | redis-support |
| Elasticsearch storage and queries | elasticsearch-support |
| Tracing instrumentation | opentelemetry-support |
| CoSec authorization | cosec-support |
See Spring Boot Starter for the complete capability table. Do not request Kafka or storage capabilities merely because they are likely future choices.
Gradle + KSP boundary
The repository's verified automatic metadata pipeline is Gradle + KSP. Maven can declare runtime dependencies, but this site does not claim an equivalent verified Maven generation path. If generated handlers are required, prove META-INF/wow-metadata.json before release.
3. Generate and Inspect Metadata
Define the bounded context, commands/events, aggregate, state, and one AggregateSpec by following Aggregate and Invariants. Keep business invariants in the aggregate, not duplicated in a controller.
Run KSP and tests in the actual annotated modules:
./gradlew clean :api:kspKotlin :domain:kspKotlin :domain:test
test -s api/build/generated/ksp/main/resources/META-INF/wow-metadata.json
test -s domain/build/generated/ksp/main/resources/META-INF/wow-metadata.jsonAdjust module paths to the application. A module with only configuration and no Wow annotations does not need a fabricated metadata file.
At runtime, MetadataSearcher merges every classpath resource named META-INF/wow-metadata.json. The server must depend on the annotated modules so their resources are packaged on its runtime classpath. Never hand-write metadata or commit build/ output.
4. Use Explicit First-Run Adapters
Wow's event store and snapshot defaults are MongoDB, while bus selection can activate external adapters when present. For a local single-process proof, select all in-memory implementations and disable PrepareKey, which requires MongoDB or Redis:
spring:
application:
name: demo-service
cosid:
machine:
enabled: true
distributor:
type: manual
manual:
machine-id: 1
generator:
enabled: true
wow:
prepare:
enabled: false
command:
bus:
type: in_memory
event:
bus:
type: in_memory
eventsourcing:
store:
storage: in_memory
snapshot:
storage: in_memory
strategy: all
state:
bus:
type: in_memoryThis is not a production configuration: data disappears on exit, delivery is single-process, and manual machine ID 1 is safe only for one instance. Persist this local profile without secrets and activate it explicitly.
5. Prove Runtime Route Wiring
Start the actual server task, for example:
./gradlew :server:bootRunRoute wiring succeeds through this chain:
- KSP resources are present on the runtime classpath;
MetadataSearchermerges bounded-context, aggregate, command, and handler metadata;- OpenAPI auto-configuration creates
RouterSpecs; - WebFlux auto-configuration registers command/state route modules;
RouterFunctionBuildermaterializes the route catalog as SpringRouterFunctions.
Verify the application logs show each annotated module's META-INF/wow-metadata.json being loaded. Then inspect /v3/api-docs or the application's route catalog and confirm the command and versioned-state paths. If Swagger UI is wanted, add it as an application choice with the matching Springdoc baseline; it is not required for the runtime route itself.
When that UI is configured, its conventional local entry is http://localhost:8080/swagger-ui.html.
6. Prove Command → Event → State
Use the generated route for the aggregate being adopted; do not add a controller as a test shortcut.
- Send a create command with a fixed aggregate ID and a unique request ID.
- Request at least
SNAPSHOTfor this adoption proof. - Require HTTP success plus
succeeded: true,errorCode: Ok, the expected stage, ID, and version. - Read
/tenant/{tenantId}/{aggregateName}/{id}/state/{version}at the returned version. - Compare the sourced state with the expected domain result and the
AggregateSpecassertion.
For a concrete verified request and response, use Getting Started. Replace its demo route and payload with the application's generated contract rather than copying Demo into production code.
Failure Checkpoints
| Observation | Check first | Do not do |
|---|---|---|
| KSP task succeeds but metadata file is absent | KSP and wow-compiler are applied in the module containing annotations | Hand-write wow-metadata.json |
| Metadata exists but no HTTP route appears | Annotated module is on the server runtime classpath; webflux-support is selected | Add a duplicate controller |
| Startup requests Kafka or MongoDB | Local profile loaded; no unwanted backend capability; all in-memory keys are set | Start unrelated infrastructure to mask wrong configuration |
| First command reports an uninitialized ID generator | CosId starter, generator, and one-instance machine ID are active | Generate production IDs with an ad hoc random fallback |
| Command returns duplicate request | Reuse of the same requestId | Disable idempotency to make the request pass |
State route is 404 | Full context/aggregate/tenant/ID, command result, and returned version | Assume HTTP 200 on the command proved state persistence |
If routes are missing, continue with Troubleshooting: Missing Metadata or Handler Registration.
Rollback Boundary
Before real traffic, rollback is code/config removal: keep the existing write path unchanged, disable the new Wow route or profile, and remove the new dependencies if the slice fails its gates.
After a Wow command has appended a domain event, rollback changes meaning:
- do not delete the event to imitate a database rollback;
- do not send the same business write through old and new paths without an explicit dual-write and reconciliation design;
- keep the old path read-only or isolated while comparing state;
- define how accepted Wow events are replayed, reconciled, or compensated before shifting traffic back;
- cut over only after backup/restore, idempotency, monitoring, and recovery are verified on the chosen durable adapters.
The migration unit should remain one aggregate/use case until its production evidence is complete.
Completion Gate
Adoption is ready for a separate cutover decision only when:
- domain tests pass and metadata files exist in every annotated module;
- the server loads those resources and exposes the generated routes;
- a real command reaches the declared wait stage;
- versioned sourced state matches the expected event history;
- failure, idempotency, storage, and rollback procedures have environment evidence.
Next Steps
- Select durable adapters: Spring Boot Starter
- Define completion semantics: Completion Semantics
- Build release gates: Testing Wow Applications
- Establish persisted-event policy: Event Evolution
- Plan module boundaries: Module Dependencies