On this page
ADR-0001: Modular monolith with thin binaries
Status: Accepted (plan decision D1). Implemented: REST, gRPC and every domain are composed in the three binaries (amended 2026-11-10).
Context
The product brief forbids unnecessary microservices. CommonLayer has many domains (identity, projects, artifacts, registry, security, maintenance, funding, GitHub, notifications) that share one database schema and one set of invariants, several of which span domains (for example: a maintainer replacement must never touch the ledger). A small operating team has to run, back up and upgrade it.
Decision
Build one Rust workspace of library crates and ship four thin binaries that share them:
commonlayer-api (HTTP, later gRPC, ops endpoints), commonlayer-worker (job runner),
commonlayer-scheduler (periodic enqueuer and business gauges) and clctl (operator
and CI tool). All four are built from the same crates and are intended to ship in one
container image. The composition root crates/commonlayer-app builds every service and
adapter from configuration.
Consequences
- One schema, one migration stream (
crates/commonlayer-database/migrations), one deployable artifact. Scaling is per process type (more API or worker replicas), not per domain. - Long work never runs in the request path: the API enqueues jobs that the worker runs (see ADR-0004).
- A defect in one domain can affect the whole process; isolation comes from crate boundaries and tests, not from process boundaries.
- As of this writing the composition root wires only the identity and organization domains; the other domains have their routers, jobs and ports ready and are being wired by the integration work package (WP-40). The gRPC listener does not exist yet (WP-41). See the implementation status.
Amendment, 2026-11-10
- Since the integration work (WP-40) every domain is composed into the API, worker and
scheduler: routers, jobs, subscribers, schedule and business gauges
(
crates/commonlayer-app). The API process also serves gRPC (WP-41, ADR-0007). - The binaries ship in two images, not one:
commonlayer/backend(api, worker, scheduler,clctl) andcommonlayer/worker, which is the backend image plus cosign, Trivy and Grype (infrastructure/docker/backend.Dockerfile, targetsruntimeandworker). The web portal has its own image. - The processes no longer share one set of secrets: each binary loads and keeps only the
configuration it uses (
Settings::load_forincrates/commonlayer-config; the per-process matrix is in the deployment guide).