[E01-S01-T01] ADR: Modular monolith #406
@@ -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