Overview
You have finished the by-example tracks for C4, DDD, Hexagonal Architecture, and Finite State Machines. You know what an aggregate is. You know what a port is. You know how to draw a Container diagram. You know how to model a state machine. Now the question is: how do they all wire together in a real production OOP codebase that ships? This case answers that question using Java 25 / Spring Boot 4 as the canonical implementation, with Kotlin, C#, and TypeScript variants, running against a hypothetical Procure-to-Pay procurement platform backend.
Every guide in this series traces a single wiring seam: how a C4 Container decomposes into bounded-context components, how a controller (Spring @RestController in Java/Kotlin, an ASP.NET Core controller in C#, or a NestJS controller in TypeScript) parses an HTTP request into a command record, how a domain aggregate processes that command through an explicit state machine, how an output port interface carries the result to a framework-managed adapter, and how a unit test swaps that adapter for an in-memory stub. No toy examples. One coherent procurement-platform-be P2P domain — production-grade wiring decisions across four OOP languages — carried consistently across all twenty-seven guides.
Prerequisites
All four of the following by-example tracks are required reading before this case:
- C4 By Example — teaches the four C4 levels (Context, Container, Component, Code) used to draw the procurement platform's architecture.
- DDD By Example in OOP — teaches DDD tactical patterns (aggregates, value objects, domain events, factories, repositories) in Java 21+, Kotlin, C#, and TypeScript using the same shared procurement-platform-be P2P domain.
- Hexagonal Architecture By Example in OOP — teaches ports-and-adapters structure (primary adapters, output port interfaces, adapter swapping, integration test seams) in Java, Kotlin, C#, and TypeScript.
- FSM By Example in OOP — teaches state-as-enum/sealed-type, transition methods on aggregates, and guard-condition encoding for aggregate lifecycles across Java, Kotlin, C#, and TypeScript.
This case does NOT re-teach C4, DDD, hexagonal, or FSM fundamentals. Terms like container, component, aggregate, port, adapter, bounded context, repository pattern, and state machine are used without definition. If any of those feel unfamiliar, complete the prerequisite tracks first. The guides here are about wiring — how the pieces connect in production — not about what the pieces are.
How the Four Families Compose in the OOP Codebase
The four architecture pattern families slot together at different levels of the codebase. Java 25 / Spring Boot 4 is the canonical implementation; the structural mapping applies to Kotlin / Spring Boot 4, C# / ASP.NET Core, and TypeScript / NestJS as well:
| Family | Java / Kotlin (Spring Boot 4) | C# (ASP.NET Core) | TypeScript (NestJS) |
|---|---|---|---|
| C4 | Diagram artifacts checked in alongside source; bounded-context names match the C4 Component names | Same | Same |
| DDD | Top-level packages per bounded context (com.procurement.platform.purchasing, etc.); aggregates as Java/Kotlin classes | Namespaces per context; aggregates as C# classes or records | Modules per context; aggregates as TypeScript classes |
| Hex | domain/ → application/ (ports + use cases) → infrastructure/ + presentation/ → Spring @Configuration | Similar layer split; Program.cs DI container as composition root | Similar layer split; NestJS Module as composition root |
| FSM | Aggregate state as a state field with enum or sealed State interface; transitions as aggregate methods | enum or discriminated union via sealed classes; same pattern | Discriminated union type aliases; transitions as class methods |
The composition root assembles concrete framework beans or DI registrations into the port slots, then the framework (Spring Boot, ASP.NET Core, or NestJS) takes over the request pipeline.
Running Domain — Procure-to-Pay Procurement Platform
Every guide reasons against the same hypothetical service: procurement-platform-be, the backend of a Procure-to-Pay (P2P) platform where employees request goods or services, managers approve, suppliers fulfill, and finance pays. Picking a single coherent domain lets the wiring decisions in Guide 15 reference the port introduced in Guide 5 and the aggregate introduced in Guide 3 without re-establishing context.
The platform is organized around six bounded contexts, introduced progressively across tiers:
| Bounded context | Aggregate root | Responsibility |
|---|---|---|
purchasing | PurchaseRequisition, PurchaseOrder | Requisition lifecycle, approval routing, PO issuance to supplier |
supplier | Supplier | Vendor master, approval state, risk score |
receiving | GoodsReceiptNote | Goods receipt against PO, quantity verification, QC flag |
invoicing | Invoice | Invoice registration, three-way matching (PO ↔ GRN ↔ Invoice) |
payments | Payment | Payment run scheduling, bank disbursement, supplier remittance |
murabaha-finance | MurabahaContract | (Optional Sharia angle) bank buys asset, resells to buyer at markup |
The intended package/namespace layout for each context follows the hexagonal split (shown in Java; Kotlin uses the same layout, C# uses namespaces, TypeScript uses NestJS modules):
com.procurement.platform.<context>/ (Java / Kotlin)
domain/ // aggregates, value objects, domain events, state machines (no framework annotations)
application/ // application services, output port interfaces
infrastructure/ // framework-managed adapters (JdbcClient/EF Core/TypeORM repos, RestClient, messaging)
presentation/ // HTTP adapter (Spring @RestController / ASP.NET Core Controller / NestJS Controller)
Cross-context domain events travel between contexts via the EventPublisher port. The most important events used across guides are summarized below; each event is reintroduced inline in the first guide that uses it.
| Event | Source context | Consumers |
|---|---|---|
RequisitionSubmitted | purchasing | approval-router |
RequisitionApproved | purchasing | purchasing (auto-converts to PO Draft) |
PurchaseOrderIssued | purchasing | supplier-notifier (EDI/email), receiving |
PurchaseOrderAcknowledged | purchasing | receiving (opens GRN expectation) |
PurchaseOrderCancelled | purchasing | supplier-notifier, accounting |
GoodsReceived | receiving | invoicing (enables matching), purchasing (state update) |
GoodsReceiptDiscrepancyDetected | receiving | invoicing (blocks matching), supplier-notifier |
InvoiceMatched | invoicing | payments (schedules payment), purchasing |
PaymentDisbursed | payments | supplier-notifier, accounting, purchasing |
SupplierApproved | supplier | purchasing (eligible-for-PO list) |
Code Grounding
Every tab block in this case shows production-grade hypothetical code in Java (canonical), Kotlin, C#, and TypeScript. That means:
- Production-grade: full error handling, observability hooks where the seam being taught calls for them, no "simplified for clarity" omissions.
- Hypothetical: no link to a real file. The snippets describe the wiring shape for the procurement platform. The shape is real; the specific file at any given commit of any real service is not the point.
- Cross-guide consistent: a port interface defined in Guide 5 keeps the same method signature in Guide 12. A package layout introduced in Guide 2 stays consistent through Guide 22. A state machine introduced in Guide 3 is referenced unchanged in Guide 18.
- Java is canonical: Java examples carry the deepest annotations and drive the narrative. Kotlin, C#, and TypeScript tabs show the same wiring seam with language-idiomatic adjustments noted inline.
If you want to see real codebases that use these patterns, look at the language-track examples in the Hexagonal Architecture By Example in OOP prerequisite.
Guide Numbering
Guides are numbered monotonically across all difficulty tiers (1, 2, 3 … 27). Guide 1 appears in the beginner tier, Guide 27 appears at the end of the advanced tier. This makes cross-references unambiguous: "see Guide 5" means the same guide regardless of which tier page you are reading.
Learning Path
- Beginner (Guides 1–7) — One context = one hexagon, per-context package layout, domain types without framework annotations, application service signatures, output port as Java interface, Spring
@RestControlleras primary adapter, Spring@Configurationas composition root. - Intermediate (Guides 8–15) — Spring Data JDBC adapter behind the repository port, in-memory test adapter, domain event publisher port, outbox event adapter, full
@RestControllerpipeline, contract codegen, cross-context ACL, composition root@Configuration. - Advanced (Guides 16–27) — Testcontainers harness, Flyway migration adapter, banking port and Spring
RestClientadapter, Resilience4j retry / circuit-breaker, Micrometer tracing observability adapter, end-to-end domain event flow, hexagonal anti-patterns, Kubernetes deployment topology, Micrometer + OTLP + Prometheus observability stack, failure-modeHealthIndicatorwiring, Flyway at deploy time, configuration adapter (Secret to typed@ConfigurationProperties).
Sibling Case
The functional programming parallel of this case uses F# / Giraffe / Npgsql (canonical), Clojure / Ring / next.jdbc, and TypeScript / Hono against the same hypothetical Procure-to-Pay procurement platform:
Both cases teach the same wiring concerns against the same domain; comparing the two side-by-side is the fastest way to see what changes when you swap object-oriented Java / Kotlin / C# / TypeScript wiring for functional F# / Clojure / Hono wiring.
Procedural Sibling — Go / Rust
The OOP wiring taught here depends on Spring Boot / ASP.NET Core / NestJS framework conventions: @Configuration reflection-driven DI, @RestController request mapping, @Transactional for transaction boundaries, framework-managed JPA / EF Core / TypeORM repositories. Real Go and Rust production codebases wire the same hexagonal pieces differently:
- Go (canonical Go DDD wiring): Three Dots Labs reference implementation; Matthew Boyle, Domain-Driven Design with Golang (Packt, 2022) —
main.goas the composition root,interfaceports, struct adapters, manual constructor wiring, chi or echo for HTTP,database/sqlfor repository adapters. - Rust: axum or actix-web for HTTP, sqlx or diesel for repositories, tokio for async,
Arc<dyn Trait + Send + Sync>for shared port handles at the composition root.
For the Go / Rust formulations of the same wiring decisions — bounded-context folder layout, output-port traits, adapter swap, in-memory test adapters, OpenTelemetry observability adapter, retry / circuit-breaker, outbox pattern — see In Procedural.
Last updated May 15, 2026