A realistic Kotlin + Spring Boot 3 HTTP service in hexagonal (ports & adapters) style, where the
domain logic is proven with bmc4j — its @BmcProof suite verifies
the safety-critical rules for every input, not a sampled few.
It is a showcase: a normal-looking microservice (controllers, JPA, validation, a stubbed upstream service) with a clearly-drawn line between the part that is machine-checked and the part that is ordinary, unproven scaffolding.
HTTP (adapter) Domain (framework-free core) Driven adapters
───────────────── ──────────────────────────── ───────────────────
OrderController ─────► OrderService ─────► UserStatusPort ◄──── StubUserStatusAdapter
ProductController ─────► RecommendationService ─► ProductRepo ◄── Legacy/New stub repos
(Jakarta-validated OrderAuthorization OrderRepo ◄── JpaOrderAdapter
request DTOs) (pure decision) StockRepo ◄── JpaStockAdapter (@Version)
value objects w/ invariants
com.example.fde.domain— the core. Zero Spring/Jakarta/JPA annotations. Value objects with constructor invariants (Money,Quantity,RecommendationScore,OrderId,UserId,OrderValue), the ports (plain interfaces), the framework-free services (OrderService,RecommendationService), and the pureOrderAuthorizationdecision.com.example.fde.adapter.http—@RestControllers + Jakarta-validated request DTOs + the outcome→HTTP-status mapping. Converts DTOs into domain value objects at the boundary.com.example.fde.adapter.persistence— JPA entities (@Versionoptimistic locking on stock; unique index onorderIdfor idempotency) and theOrderRepo/StockRepoadapters.com.example.fde.adapter.stub— stub upstream user-status service + the two stub product catalogues.com.example.fde.config— the composition root that hand-wires the framework-free services from the adapter beans (the only place the domain meets Spring).
- User status ∈
{OK, ORDERS_BLOCKED, ORDERS_RESTRICTED}(fetched from a stubbed upstream service). - Order authorization (
OrderAuthorization.decide, a pure function):ORDERS_BLOCKED→ never allowed.- insufficient stock → blocked.
ORDERS_RESTRICTED→ allowed iff order value ≤ $50 (the low-value cap); value > $50 → blocked.OK(with stock) → allowed.- the decision is total — every
(status, value, stock)maps to a defined outcome, never throws.
- Idempotency —
POST /orderswith anorderIdthat already exists returns 202 Accepted. This rests on the DB unique index: the adapter maps a duplicate-key violation (DataIntegrityViolationException) →ORDER_ALREADY_EXISTS→ 202, rather than read-then-write. - Stock — decremented under optimistic locking (
@Version); an order is blocked if stock is insufficient. - Products —
GET /products/recommendedmerges the legacy + new catalogues (3 each) and sorts byrecommendationScoredescending, in code.
| Method | Path | Outcome → status |
|---|---|---|
POST |
/orders |
placed → 201 · duplicate → 202 · status-blocked / restricted-over-cap → 403 · out-of-stock → 409 · invalid body → 400 |
DELETE |
/orders/{orderId} |
204 / 404 |
GET |
/products/recommended?userId=… |
200 (merged + sorted) |
The stub user-status adapter derives status from the id prefix so the API is exercisable:
blk… → blocked, rst… → restricted, anything else → OK.
This is the point of the showcase — the boundary is explicit.
PROVEN (machine-checked, for every input):
- Value-object invariants — every constructible
Money/OrderValueis≥ 0, everyQuantityis> 0, and each invariant is genuinely enforced (construction throws exactly on the rejected inputs). - Jakarta validation → domain precondition — a request the validation layer accepts satisfies the
domain value objects' preconditions (
quantity ≥ 1,valueCents ≥ 0), via thebmc-constraints-jakartaprocessor's generatedassumeValid. - The order-authorization decision — the headline safety proof.
OrderAuthorization.decideproven clause-by-clause (ORDERS_BLOCKED⇒ never allowed;ORDERS_RESTRICTED ∧ value > $50⇒ blocked,≤ $50⇒ allowed;OK⇒ allowed; no-stock ⇒ blocked) and total. - Idempotency → 202 (the outcome contract) — a duplicate
orderIdsignalled by the repo flows through the realOrderService(controller → service → stub-repo, hand-wired) to theALREADY_EXISTSoutcome, and the total outcome→status mapper sendsALREADY_EXISTS→ 202 (and every outcome to a defined 2xx–4xx code — no domain outcome leaks a 5xx). A pinned-REFUTED demo guards that the idempotent replay never silently becomes a 201. - Stock decrement decision/arithmetic —
StockDecrement.decideproven: never decide to oversell (available < qty⇒ refused), an allowed decrement is exactlyavailable − qtyand never negative, and the allow/deny boundary is exactly the stock check. Sequential framing (honest): jbmc is a sequential model checker, so this proves the application-level decision + arithmetic, not multithreaded race-freedom — the actual atomicity under concurrency is the DB's@Versionoptimistic lock; bmc4j proves the logic around that lock is correct. - Products merge/sort — the recommendation merge is proven sorted descending by score and a permutation of the union (size preserved + multiset-equal). Tractability bound: proven over a bounded 2 + 2 = 4-element fully-symbolic-score input (production is 3 + 3 = 6); the property is size-bounded and depends only on the score key, so this demonstrates the algorithm for all score values. A pinned-REFUTED demo guards the sort direction.
NOT proven (ordinary, trusted scaffolding):
- the Spring HTTP layer, Jackson (de)serialization, JPA/Hibernate, and the H2 database;
- the stubbed upstream user-status service (its mapping is a stub, out of the proof boundary);
- the actual persistence/optimistic-lock/idempotency execution (the DB enforces these at runtime — bmc4j proves the application-level decisions, the DB provides the concurrency/uniqueness atomicity).
The proofs cover the domain logic + value invariants + validation contract + the error→status mapping's totality — the parts where a bug is a correctness/safety defect, not an integration detail.
bmc4j analyzes JVM bytecode and runs as an ordinary Gradle plugin: each @BmcProof is a JUnit 5
test, so ./gradlew test runs the proof suite alongside any Spring tests. The
bmc-constraints-jakarta annotation processor reads the validated bean
(PlaceOrderForm, a Java mirror of the Kotlin DTO — the processor is a Java APT) and generates
PlaceOrderFormConstraints.assumeValid(...), which the validation proof uses.
CI (.github/workflows/proofs.yml) runs every proof with -Dbmc.summaryDir=… and posts a per-proof
report (Expected | Actual | Counterexample) as a PR comment — mirroring bmc4j's own CI report. A couple
of proofs are pinned @BmcProof(expect = REFUTED): they assert a deliberately-false claim and pass by
being refuted, so the report surfaces the counterexample bmc4j found (e.g. quantity = 0) and a
regression that made the claim un-refutable would fail the build.
29 proofs → 29 verified, 0 refuted, 0 unknown
(25 VERIFIED + 4 REFUTED-as-expected, each in ~6–23s.)
./gradlew bootRun # starts on :8080 against an in-memory H2 DB (seeded stock)
./gradlew test # runs the @BmcProof suite (and any tests)bmc4j is consumed from a GitHub Packages snapshot (its pre-Central channel), which needs an
authenticated token with read:packages even though the packages are public — in CI the workflow's own
GITHUB_TOKEN; locally gh auth token (if scoped) or -Pgpr.user=<you> -Pgpr.token=<PAT>. To validate
proofs offline against a publishToMavenLocal dev build of bmc4j, pass -PbmcVersion=0.0.1-local
(mavenLocal is first in settings.gradle.kts, so no token is needed).
Consumer Kotlin 2.4 is the floor (bmc4j 0.4.x); the build runs on JDK 21.
Apache-2.0 (matching bmc4j).