Skip to content

08 — Dev validation on your machine

How to prove the base architecture on your M1 laptop: 50 000 contracts, continuous lapse (no month-end), reporting over JSONB, and messaging from the outbox.

Prerequisites: Docker Desktop running, JDK 17+, k6 installed.


1. Start dev mode

cd backend/limen-server
export JAVA_HOME=~/.sdkman/candidates/java/21.0.4-amzn   # or your JDK 17+
mvn quarkus:dev

Swagger: http://localhost:8080/api/swagger-ui


2. Prove continuous lapse (not month-end batch)

Activate with a lapse milestone a few seconds in the future:

curl -s -X POST http://localhost:8080/api/contracts/LMN-LAPSE-TEST/activations \
  -H 'Content-Type: application/json' \
  -d '{
    "effectiveAt": "2025-11-01T00:00:00Z",
    "lapseAt": "'$(date -u -v+5S +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '+5 seconds' +%Y-%m-%dT%H:%M:%SZ)'"
  }' | jq .

Wait 10 seconds, then:

curl -s http://localhost:8080/api/contracts/LMN-LAPSE-TEST | jq .status
curl -s http://localhost:8080/api/milestones/statistics | jq .

Expect: LAPSED, processed count increased. No batch job was started — MilestoneDispatch polled due rows with SKIP LOCKED.


3. Seed 50 000 contracts (dev instance)

In a second terminal while quarkus:dev is running. Run in the foreground so k6 prints progress:

cd backend/limen-server
./scripts/load.sh seed 50000

If you background it (&), watch it with:

tail -f backend/limen-server/target/load-seed.log
./scripts/load-status.sh

Done when: load-status.sh shows finished successfully and book-summary reports ~50 000 contracts.

Takes ~2–3 minutes on an M1.


4. Reporting — CDC projections (phase two proof)

Now: /api/reporting/* reads from reporting_proj — physical tables fed asynchronously from the outbox stream. Reporting never touches contract, contract_version or ledger_entry.

Primary (write)  →  outbox  →  Kafka  →  ProjectionConsumer  →  reporting_proj
                     ↑                                              ↓
              same transaction                              /api/reporting/*

Production swaps application dispatch for Debezium on the WAL; the consumer and projection tables stay the same.

Prove it on your running dev instance

Restart quarkus:dev first (Flyway applies V1.2.0__cdc_projections.sql).

# One-shot: align projections with your existing 50k seed
curl -s -X POST http://localhost:8080/api/reporting/projection/backfill | jq .

# Compliance watermark — primary views vs projection store
curl -s http://localhost:8080/api/reporting/reconciliation | jq .
curl -s http://localhost:8080/api/reporting/projection/status | jq .

# Reporting now reads the projection store
curl -s http://localhost:8080/api/reporting/book-summary | jq .
curl -s http://localhost:8080/api/reporting/components | jq '.[0:3]'

Or run the full walkthrough:

./scripts/prove-cdc.sh

Expect: reconciliation.matched: true, eventsProcessed increasing as you quote/activate/lapse, component rows updating without querying primary tables.

Endpoint Source Purpose
/reporting/components reporting_proj.component_row Actuarial / ops flatten
/reporting/ledger-summary reporting_proj.ledger_bucket Finance / IFRS 17 actuals
/reporting/reconciliation Compares views vs projections Compliance watermark
POST /reporting/projection/backfill Copies primary views → projections Bulk seed bootstrap

There is no contract_component table. The snapshot is truth; the projection is the reporting read-model contract.


4b. Reporting — what changed from phase one

Phase one queried SQL views on the primary database. That proved JSONB shape. Phase two proves isolation: finance and compliance queries do not compete with the write path.

curl -s http://localhost:8080/api/reporting/book-summary | jq .
curl -s http://localhost:8080/api/reporting/components | jq '.[0:3]'
curl -s http://localhost:8080/api/reporting/ledger-summary | jq '.[0:5]'
Endpoint Phase-one source Now
/reporting/components View over primary JSONB CDC-fed reporting_proj
/reporting/ledger-summary Aggregate on primary ledger CDC-fed reporting_proj

Production target remains ClickHouse / read replicas for scale; the pattern is proven here.


5. Messaging (outbox → channel)

Events are written to outbox_event in the same transaction as state changes. OutboxDispatch publishes to the limen-events channel (in-memory in dev, Kafka in prod).

Watch dev logs for:

channel contract.quoted for LMN-...
channel contract.status.changed for LMN-...

Or query Postgres:

docker ps | grep postgres   # find Dev Services container
docker exec -it <container> psql -U <user> -d <db> \
  -c "select count(*) filter (where published_at is null) awaiting, count(*) published from outbox_event"

6. What each proof answers

Question How you know
Do we need month-end batch for lapse? Milestone test — one row, continuous dispatch
Can we run 50k on a laptop? load.sh seed + snapshot.sh
Is Redis worth it? load.sh read — compare hot vs truth p50
Can finance report without normalised components? /reporting/components view over JSONB
Do events leave the transaction safely? Outbox row + channel log + published_at set

7. Automated suite

mvn verify

Includes ContinuousMilestoneTest, ReportingJsonbTest, and the original architecture tests.