docs(adr): record modular monolith decision as ADR-001
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

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.
This commit is contained in:
2026-08-31 00:12:48 +00:00
parent aaa7489566
commit 41bd4d78aa
+107
View File
@@ -0,0 +1,107 @@
# 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.