Files
PersonalBlog/docs/adr/ADR-001-modular-monolith.md
bot-implementer 41bd4d78aa
CI / Stage 5 — Architecture tests (E00-S05-T01) (pull_request) Successful in 3m7s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (pull_request) Successful in 1m3s
CI / Stage 2 — Typecheck (E00-S05-T01) (pull_request) Successful in 1m12s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (pull_request) Successful in 45s
CI / Stage 4 — Unit tests (E00-S05-T01) (pull_request) Successful in 1m41s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (pull_request) Successful in 1m8s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (pull_request) Successful in 1m41s
docs(adr): record modular monolith decision as ADR-001
Adds docs/adr/ADR-001-modular-monolith.md with the six required sections
(Context, Decision, Alternatives, Consequences, Operational impact, Revisit
trigger). The decision text ("Modular monolith") matches the ADR index entry
in section 70. Documentation-only; no runtime or schema impact.
2026-08-31 00:12:48 +00:00

5.7 KiB

ADR-001: Modular monolith

  • Status: Accepted
  • Date: 2026-08-31
  • Deciders: platform stream
  • References: ADR index (section 70), Architecture wiki (sections 1, 4, 9)

Context

EPPP is a greenfield personal blogging platform. The first visible release (v0.1) is intentionally small: editable site identity, editable Home page, ordered list of published posts, individual post pages, an administration interface and the Amber theme. It must be small because optional features are absent, not because extensibility is absent — the codebase ships explicit, versioned boundaries from day one for content types, content blocks, page sections, themes, extensions, extension settings, navigation, visitor preferences, database migrations, media storage, background jobs, events and rendering.

The platform is owned by a single small stream, operates a single canonical PostgreSQL database, and has no v1 requirement for independent service deployment, service discovery, message brokers, distributed transactions or cross-service schema versioning. The workspace is a pnpm monorepo whose dependency boundaries are already CI-enforced (FIT-001, FIT-010, FIT-011), so module boundaries can be structural, explicit and testable rather than purely organisational.

Decision

EPPP is a modular monolith. One application image contains the public server, admin API, rendering pipeline, extension runtime, core services, built admin assets and first-party extensions, deployed as a single deployable unit; an optional worker uses the same image with a different command (eppp serve / eppp worker). Runtime is Node.js 24 LTS with Fastify 5, PostgreSQL 18 as the sole canonical database, React 19 for server-rendered public components and a Vite admin, and Docker Compose as the primary runtime model.

Modules are separated in the workspace layout (apps/, packages/, extensions/) and their import edges are enforced by dependency-boundaries.json in CI. Every module boundary is an explicit, versioned contract; no module may reach into another module's internals. The decision text — Modular monolith — matches the ADR index entry (ADR-001, section 70).

Alternatives

  • Microservices / distributed system — rejected: no v1 requirement justifies distributed transactions, service discovery, brokers, multi-service deployment or cross-service schema changes; the operational and cognitive cost is disproportionate for a small platform stream, and it would split one canonical database into many.
  • Classic (unstructured) monolith — rejected: it would undermine the extension architecture, which depends on stable versioned contracts between core, extensions and themes, and it would make the CI-enforced dependency boundaries impossible to honour in practice.
  • Serverless / FaaS — rejected: it conflicts with the chosen long-lived runtime model (Fastify 5 process, background jobs, connection-backed PostgreSQL access) and the single-image deployment model.
  • Modular monolith with future extraction — chosen: module boundaries exist now; any module can be extracted into a service later on evidence without a rewrite of the whole system.

Consequences

  • Positive: one deployable unit means simpler operations, atomic deploys and a single image to build, scan and promote; all modules share one transaction and consistency boundary via the canonical database; a small team can own the entire platform; explicit boundaries force API discipline and keep the extension contracts honest.
  • Negative: the single process only stays modular through discipline — a boundary violation degrades it toward a big ball of mud, which is exactly what the CI dependency-boundary tests guard against; scaling is vertical until a module is extracted; deploys are all-or-nothing; a fault in one module can affect the whole process (mitigated by the optional worker separation for background jobs and by health/readiness gates).
  • Neutral: extraction of a module into a service remains possible and is a deliberate, evidence-based decision rather than a default; the monorepo layout already supports it because modules are physically separated.

Operational impact

  • One application image/container for the server plus an optional worker container running the same image with a different command; no service discovery, message broker or per-service observability requirements at v1.
  • Single PostgreSQL 18 instance as the sole canonical database with one core migration chain; extension-owned tables live in independent chains (eppp_extension_migrations), all run behind the advisory migration lock at startup.
  • The health/readiness endpoint gates on migration completion; readiness is reported only when the single deployable unit is fully initialised.
  • Deploy is build-one-image-and-run-Compose; rollback is redeploying the previous image. Horizontal scaling means running more instances of the same image behind a proxy for stateless work and more workers for background jobs; the database remains the single shared store.

Revisit trigger

  • Revisit this ADR when a module's change frequency, team ownership or scaling needs diverge enough that a single deployable unit becomes a bottleneck — for example, when a module needs an independent deploy cadence, independent scaling or a different runtime — and there is evidence (per the architecture review gates, ADR index section 68) that extracting that module into a service would help.
  • Revisit if the platform grows to multiple independent products or teams requiring distributed transactions or independent data stores, or if the v1.1 boundary contract (ADR-027 to ADR-032) pressures the single-image model.