Getting Started
Use the Wow Project Template to prove one complete vertical slice before replacing the demo domain:
domain test → generated route → HTTP command → SNAPSHOT wait → state at version 1This path uses the template's checked-in in-memory buses, event store, and snapshot store. It does not require Kafka, MongoDB, or Redis.
Verified Baseline
This page was executed on 2026-08-27 against template commit 1dc9267b7f276c8c3bd9b2fad3186e3e3c3e82f9:
| Component | Verified value |
|---|---|
| Template Wow version | 8.13.0 |
| Spring Boot | 4.1.1 |
| Kotlin | 2.4.10 |
| KSP | 2.3.11 |
| Gradle Wrapper | 9.7.1 |
| JDK used for the run | 17.0.7 |
The current Wow documentation source is 9.0.0, while the verified template pins 8.13.0. The repositories evolve independently. Always inspect the cloned template's gradle/libs.versions.toml; change versions only as one reviewed compatibility baseline for a selected Wow release.
Before You Start
- JDK 17 or later
- Git
curl- the checked-in Gradle Wrapper; no global Gradle installation is required
Use a disposable clone for the first pass so local runtime files cannot pollute an application repository:
git clone https://github.com/Ahoo-Wang/wow-project-template.git
cd wow-project-template
git fetch --depth 1 origin 1dc9267b7f276c8c3bd9b2fad3186e3e3c3e82f9
git checkout --detach FETCH_HEAD
git rev-parse HEAD
grep '^wow = ' gradle/libs.versions.tomlThe checkout command pins the exact commit used for the expected results below. If you intentionally keep a different template HEAD, do not mix it with this page's fixed expectations: repeat the domain check, startup, route, command, and versioned-state validation against that HEAD and record the new baseline. If you create a repository with the template button instead, rename rootProject.name in settings.gradle.kts after the first successful run.
The 30-Minute Target Path
The functional path below has been exercised end to end. Thirty minutes remains a target because first-time developer wall-clock completion has not been measured; this is not a completed human usability study.
1. Prove the Domain Model
Run the exact module check defined by the template:
./gradlew :domain:check --console=plainThe check compiles KSP metadata, runs DemoSpec and DemoSagaSpec, and enforces the domain coverage rule. Success ends with BUILD SUCCESSFUL.
In the verification environment, the first run exhausted Gradle's default 384 MiB Metaspace during :api:kspKotlin. If the failure is specifically OutOfMemoryError: Metaspace, retry without editing the template:
./gradlew :domain:check --console=plain \
-Dorg.gradle.jvmargs='-Xmx1g -XX:MaxMetaspaceSize=1g'Do not use this retry to hide a compilation, test, coverage, or dependency failure.
The behavior under test comes from the template itself:
DemoreturnsDemoCreatedandDemoUpdated;DemoStateapplies those events;DemoSpecverifies command → event → state;DemoSagareacts to creation with an update command.
2. Start the Service with Versioned Configuration
The template's :server:run task uses server/ as its working directory and expects server/config/. Point that path to the checked-in server/src/main/resources instead of copying configuration:
mkdir -p server/logs
test -e server/config || ln -s src/main/resources server/config
./gradlew :server:run --console=plainIf the earlier Metaspace failure occurred, apply the same command-line JVM setting to :server:run. Wait until the log contains both:
Netty started on port 8080 (http)
Started ServerKtThe sourced configuration is server/src/main/resources/application.yaml. It selects in_memory command/event buses, event store, snapshot store, and state-event bus, plus a manual CosId machine ID for one local instance.
server/config and server/logs/ are local runtime artifacts and are not ignored by the verified template. A Metaspace crash may also leave an untracked heap dump such as java_pid*.hprof in the repository root. Before committing an application repository, inspect all three with:
git status --short -- '*.hprof' server/config server/logsRemove only the listed local artifacts after inspection. On Windows, create an equivalent directory link or point spring.config.location at the checked-in resource directory for the local run.
Open http://localhost:8080/swagger-ui.html. The generated OpenAPI should include POST /tenant/{tenantId}/demo and /tenant/{tenantId}/demo/{id}/state/{version}.
3. Send the First Real Command
Keep the service running and execute this in another terminal:
curl -sS -X POST \
'http://localhost:8080/tenant/tenant-1/demo' \
-H 'accept: application/json' \
-H 'Command-Wait-Stage: SNAPSHOT' \
-H 'Command-Aggregate-Id: demo-1' \
-H 'Command-Request-Id: quickstart-demo-1' \
-H 'Content-Type: application/json' \
-d '{"data":"hello-wow"}'The generated route comes from the template's CreateDemo. Verify these response fields rather than relying on HTTP 200 alone:
{
"stage": "SNAPSHOT",
"aggregateId": "demo-1",
"aggregateVersion": 1,
"requestId": "quickstart-demo-1",
"errorCode": "Ok",
"succeeded": true
}The response contains additional generated IDs and timing data; those values vary by run.
4. Read the Versioned Sourced State
Read the aggregate at exactly version 1:
curl -sS \
'http://localhost:8080/tenant/tenant-1/demo/demo-1/state/1' \
-H 'accept: application/json'The verified response is:
{"id":"demo-1","data":"hello-wow"}This proves the CreateDemo route, aggregate decision, DemoCreated persistence, sourcing handler, snapshot wait, and versioned state reconstruction. It also avoids a race with DemoSaga: that saga subsequently sends UpdateDemo(data = "updated"), so the unversioned current-state endpoint eventually returns:
curl -sS \
'http://localhost:8080/tenant/tenant-1/demo/demo-1/state' \
-H 'accept: application/json'{"id":"demo-1","data":"updated"}Repeating the path
Command-Request-Id is the idempotency key. For another trial, change both the request ID and aggregate ID, or restart the service to clear its in-memory data.
Completion Gate
The first slice is complete only when all five observations hold:
:domain:checksucceeds;- startup loads the generated
META-INF/wow-metadata.jsonresources and listens on 8080; - Swagger/OpenAPI contains the generated command and versioned-state routes;
- the HTTP command returns
succeeded: true,stage: SNAPSHOT, and aggregate version1; - the version
1state is exactly{"id":"demo-1","data":"hello-wow"}.
Replace the Demo Safely
The template's module responsibilities are the migration path:
| Module/path | Replace or preserve |
|---|---|
api | Replace demo commands/events and update DemoService aggregate metadata |
domain | Replace Demo, DemoState, DemoSaga, and their specs together |
server | Preserve runtime wiring; add only extensions required by the target environment |
server/src/main/resources | Keep configuration versioned; split environment values without committing secrets |
gradle/libs.versions.toml | Pin one tested dependency baseline |
After each domain change, rerun ./gradlew :domain:check. Do not add Kafka, MongoDB, Redis, or Elasticsearch until durable storage, multi-instance messaging, or a specific query backend is an actual requirement.
The template also wires CosId for IDs and contains a TypeScript client generated with Fetcher; keep or remove those pieces according to the application's published contract.
Next Steps
- Understand the terms used above: Core Concepts
- Replace the demo model: Aggregate and Invariants
- Choose completion semantics: Completion Semantics
- Add application gates: Testing Wow Applications
- Build a read model: Projection and Query
- Select runtime integrations: Configuration and Spring Boot Starter