Merge pull request '[E01-S01-T01] ADR: Modular monolith' (#406) from feature/188 into main
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (push) Successful in 1m11s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (push) Successful in 1m3s
CI / Stage 5 — Architecture tests (E00-S05-T01) (push) Successful in 3m14s
CI / Stage 2 — Typecheck (E00-S05-T01) (push) Successful in 1m9s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (push) Successful in 45s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (push) Successful in 1m42s
CI / Stage 4 — Unit tests (E00-S05-T01) (push) Successful in 1m35s
CI / Stage 6 — PostgreSQL integration tests (E00-S05-T01) (push) Successful in 1m11s
CI / Stage 7 — Build the admin and server applications (E00-S05-T01) (push) Successful in 1m3s
CI / Stage 5 — Architecture tests (E00-S05-T01) (push) Successful in 3m14s
CI / Stage 2 — Typecheck (E00-S05-T01) (push) Successful in 1m9s
CI / Stage 3 — Formatting/lint policy (E00-S05-T01) (push) Successful in 45s
CI / Stage 1 — Frozen lockfile install (E00-S05-T01) (push) Successful in 1m42s
CI / Stage 4 — Unit tests (E00-S05-T01) (push) Successful in 1m35s
This commit was merged in pull request #406.
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user