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 codebase that ships? This case answers that question across four functional languages — F# / Giraffe / Npgsql (canonical), Clojure / Ring / next.jdbc, TypeScript / Hono / node-postgres, and Haskell / Servant / postgresql-simple — all running against the same hypothetical Procure-to-Pay procurement platform.
Every guide in this series traces a single wiring seam: how a C4 Container decomposes into bounded-context components, how a handler parses an HTTP request into a command, how a domain aggregate processes that command through an explicit state machine, how an output port carries the result to a database adapter, and how an integration test swaps that adapter for an in-memory stub. Each guide presents the seam in all four languages as parallel tabs. F# is the canonical tab — it carries the deepest annotations and the framing prose — while Clojure, TypeScript, and Haskell are first-class variants showing the same wiring decisions in their respective idioms. No toy examples. One coherent procurement-platform-be P2P domain — production-grade wiring decisions — 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 FP — teaches DDD tactical patterns (aggregates, value objects, domain events, workflows) in F#, Clojure, TypeScript, and Haskell using the same shared procurement-platform-be P2P domain.
- Hexagonal Architecture By Example in FP — teaches ports-and-adapters structure (primary adapters, output ports, adapter swapping, integration test seams) in F#, Clojure, TypeScript, and Haskell.
- FSM By Example in FP — teaches state types as discriminated unions / keyword enums / union types / ADTs and transition functions guarding aggregate lifecycles.
You should be comfortable reading at least one of F#, Clojure, TypeScript, or Haskell before starting. The guides present all four languages as parallel tabs; you can follow whichever language matches the stack you ship.
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 Across the Three Codebases
The four architecture pattern families slot together at different levels of each codebase. The table below shows the F# (canonical) shape; the Clojure, TypeScript, and Haskell tabs in each guide show the equivalent idiom.
| Family | F# (canonical) | Clojure variant | TypeScript variant | Haskell variant |
|---|---|---|---|---|
| C4 | Diagram artifacts checked in alongside source; bounded-context names match the C4 Component names | Same diagram artifacts; namespace maps match C4 Component names | Same diagram artifacts; module directories match C4 Component names | Same diagram artifacts; module directories (PascalCase) match C4 Component names |
| DDD | Top-level modules per bounded context (ProcurementPlatformBe.Purchasing, etc.); aggregates as records | Namespaced maps and spec-validated data; aggregates as plain maps | Typed modules per bounded context; aggregates as plain objects + Zod | Top-level modules per bounded context; aggregates as data records with smart constructors |
| Hex | Domain.fs (core) → Application/Ports.fs + *Workflow.fs → Adapters/{In,Out} → CompositionRoot.fs | domain/ → application/ports.clj → adapters/{in,out}/ → system.clj | domain/ → application/ports.ts → adapters/{in,out}/ → composition-root.ts | Domain/ → Application/Ports.hs → Adapters/{In,Out}/ → CompositionRoot.hs |
| FSM | Aggregate state as a discriminated union; transitions as pure functions returning Result<NextState, _> | State as a keyword enum; transitions as pure multimethods | State as a union type; transitions as pure functions returning Result | State as a sum-type ADT; transitions as pure functions returning Either Err NextState |
The composition root in each language assembles concrete adapter implementations into the port slots before the HTTP framework takes over the request pipeline.
Rust as an FP-Adjacent Member — With Concept Adjustments
Rust shares deep DNA with the FP languages on this case: enum is a sum type (Blandy & Orendorff, Programming Rust, Ch. 10), traits cover much of the territory typeclasses cover, and Result<T, E> is the Railway-Oriented Programming primitive natively. The wiring decisions taught here — bounded-context folder layout, output ports as traits, adapter swap at the composition root, in-memory test adapters, OpenTelemetry observability adapter, retry / circuit-breaker decorators, outbox pattern — translate to Rust (axum / actix-web for HTTP, sqlx / diesel for repository, tokio for async). Patterns where ownership is the wiring force (move-on-call ports, typestate-encoded request lifecycles) live in the in-procedural case.
Six concept adjustments when you read this case through a Rust lens (axum / sqlx / tokio):
- Ownership / affine types — no F# equivalent. Port method signatures explicitly choose between consuming (
self), shared-borrow (&self), and exclusive-borrow (&mut self); the choice is a wiring decision F# does not have to make. (Source: without.boats — Ownership) - No higher-kinded types (HKT). Rust cannot abstract over "any effect monad implementing this port"; output ports are traits over concrete effect types.
async fnin traits (stabilised in Rust 1.75) closes part of the gap. (Source: GAT stabilisation — Matsakis & Huey) ?operator is sugar, not monadic bind. Hexagonal application services that chain port calls in F# useResult.bindorasyncResult { }; in Rust they use.await?repeatedly. Same Railway-Oriented effect, different conceptual machinery. (Source: Rust By Example —?operator)async/Futureis not a monad. F#Async<Result<>>composition uses computation expressions; Rust usesasync fn+.await+?and concreteFuturestate machines.tokio::try_join!provides ad-hoc parallel-composition combinators.- No persistent immutable shared structures by default. Composition roots in F# wire records-of-functions captured by closures; in Rust, dependencies are held in
Arc<dyn Trait + Send + Sync>for shared ownership across the tokio runtime. - Traits ≈ typeclasses minus HKT. Output ports as
#[async_trait] trait Repository { ... }. Adapter implementations satisfy the trait. Application services acceptArc<dyn Repository + Send + Sync>or genericR: Repository. Type-level dispatch akin to typeclasses, minus kind-polymorphism.
Where the FP wiring idiom translates one-to-one, Rust appears as an additional language tab alongside F# / Clojure / TypeScript / Haskell. Where the wiring requires an ownership-driven re-formulation (move-on-call ports, typestate-encoded request pipelines, Send + Sync bounds at the composition root), the guide points to the procedural case instead.
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. Employees request goods and services, managers approve, suppliers fulfill, and finance pays. Picking a single coherent domain (instead of one toy example per guide) lets the wiring decisions in Guide 14 reference the port introduced in Guide 5 and the aggregate introduced in Guide 3 without re-establishing context.
The platform organizes around six bounded contexts, introduced progressively by tier:
| Bounded context | Aggregate root | Responsibility |
|---|---|---|
purchasing | 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 |
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 |
|---|---|---|
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 |
InvoiceDisputed | invoicing | supplier-notifier, accounting |
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 F# (canonical), Clojure, TypeScript, and Haskell code. That means:
- Production-grade: full error handling, observability hooks where the seam being taught calls for them, no "simplified for clarity" omissions — in all four language tabs.
- 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 defined in Guide 5 keeps the same signature in Guide 11 across all four language tabs. A bounded-context folder shape introduced in Guide 2 stays consistent through Guide 22. A state machine introduced in Guide 3 is referenced unchanged in Guide 18.
- F# is canonical: the F# tab carries the deepest annotations and the framing narrative. Clojure, TypeScript, and Haskell tabs show the same wiring in idiomatic form with equivalent annotation density but shorter explanatory prose.
If you want to see real codebases that use these patterns, look at the F#, Clojure, TypeScript, and Haskell track examples in the Hexagonal Architecture By Example in FP 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–6) — One context = one hexagon, per-context folder layout, domain types without framework imports, application service signatures, output port as F# function type alias, Giraffe handler as primary adapter.
- Intermediate (Guides 7–14) — Npgsql adapter behind the repository port, in-memory test adapter, domain event publisher port, outbox adapter, full Giraffe pipeline, contract codegen, cross-context ACL, composition root in
Program.fs. - Advanced (Guides 15–27) — docker-compose integration harness, DbUp migrations, banking port and payment adapter, retry and circuit-breaker, end-to-end domain event flow, OpenTelemetry observability adapter, murabaha-finance optional context, hexagonal anti-patterns, Kubernetes deployment topology, OpenTelemetry deployment wiring, failure-mode degraded adapters, configuration adapter at the deploy seam, background job adapter.
Sibling Case
The object-oriented parallel of this case uses Java 25 / Spring Boot 4 (canonical), Kotlin / Spring Boot, C# / ASP.NET Core, and TypeScript / NestJS against the same hypothetical 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 functional wiring (F#, Clojure, TypeScript functional style, Haskell) for object-oriented wiring (Java, Kotlin, C#, NestJS).
Last updated May 15, 2026