Skip to content

About

Spec-driven, domain-driven modular monolith in miniature: specs lead, bounded contexts talk through events, architecture and traceability enforced as build gates.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Spec-driven DDD — a modular monolith in miniature

ci python license

A small library-lending system that exists to show how software gets built when the specification leads and the architecture is enforced by the build — not the size of the system.

The domain is deliberately familiar (books, copies, members, loans, late fees) so the interesting parts are the ones around it:

  • Specs lead. Behaviour is written first as numbered rules (specs/features/); code and tests cite those ids.
  • Bounded contexts that cannot leak. catalog and lending never import each other; they exchange integration events. A build failure, not a wiki page.
  • Rules as plain functions. The domain layer has no framework, no repository, no clock — every rule is a unit test away.
  • Single source for policy. Loan limits and fees live in specs/policy.yaml; the code carries a mirror that the build checks byte for byte.
  • Traceability that fails. Every spec rule must have a test tagged with its id, and every tag must name a real rule.
  • Contracts in review. The OpenAPI document is a committed snapshot; an API change without it fails CI.
flowchart TB
  subgraph specs["specs/ (leads)"]
    R["features/*: rules LEND-R1..R7, CAT-R1..R2"]
    P["policy.yaml"]
  end
  subgraph app["src/library (one deployable)"]
    direction LR
    subgraph catalog
      CA[api] --> CAp[application] --> CD[domain]
    end
    subgraph lending
      LA[api] --> LAp[application] --> LD[domain]
    end
    CAp -- CopyRegistered --> LAp
    LAp -- LoanOpened / LoanClosed --> CAp
  end
  P -. mirrored, byte-checked .-> LD
  R -. "@pytest.mark.rule(id)" .-> T[tests]
Loading

Quickstart

make install     # uv sync
make check       # every gate CI runs
make run         # http://127.0.0.1:8000/docs
docker build -t library-sample . && docker run -p 8000:8000 library-sample

Every merge to main publishes ghcr.io/hasanozkan/library-sample:main-<unix-ts>-<sha>, which gitops-reference deploys.

The gates, and what each one proves

make … Fails when
imports a context imports another · a layer imports upwards · the domain imports FastAPI/Pydantic
mirror the code's policy differs from specs/policy.yaml
trace a spec rule has no test · a test cites a rule no spec defines
test a rule does not hold
contracts the API changed but contracts/openapi.json did not
lint, types ruff, strict mypy
CI secrets gitleaks finds a secret in history

Each gate was verified by breaking it on purpose once — a cross-context import, a framework in the domain, a drifted policy, an untagged rule, an unsnapshotted route, a removed fee cap. All six turned the build red.

Operability — telemetry is part of the contract

specs/telemetry.yaml names what the service emits (OpenTelemetry; http.server.request.duration from the semantic conventions, library.* for the domain), and feature 004 makes it rules: requests timed by route template (OPS-R1), loans and late fees counted from the integration events so the contexts never know they are measured (OPS-R2), refusals by problem code (OPS-R3), all at GET /metrics (OPS-R4). The test reads the same YAML the .NET implementation is held to — rename a metric in one and its build turns red.

Tour

Path What to look at
docs/workflow.md The order of work: spec → decision → slice → gate, DoR / DoD
docs/adr/ Why a modular monolith, why events between contexts, why policy is data
specs/domain/ Bounded contexts and the ubiquitous language
src/library/lending/domain/rules.py The rules, each naming its spec id
src/library/contracts/events.py The only thing the contexts share
src/library/app.py The composition root — the one place that knows every context
scripts/ The trace, mirror and contract checks (a few dozen lines each)
AGENTS.md The same rules, written for an AI coding agent

Deliberate simplifications

In-memory repositories and a synchronous in-process event bus. In a production system the repositories would sit on a database and events would leave through a transactional outbox to a broker — the domain, the use cases and the contracts would not change. That is the point of the boundaries.


Built by Hasan Özkan. The patterns here come from building a production product with spec-driven, agent-assisted development; this repository is an independent, from-scratch illustration of them.

About

Spec-driven, domain-driven modular monolith in miniature: specs lead, bounded contexts talk through events, architecture and traceability enforced as build gates.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages