Summary: The elixir-backend is structured as a modulith — a single deployable unit organized as a set of strictly isolated domains that communicate only through events and explicit public interfaces. Sources: direct code inspection (lib/backend/, guides/structure.md, mix.exs) Last updated: 2026-05-15
What it is
A modulith applies microservice design discipline (domain isolation, explicit interfaces, no shared state) inside a single Elixir release. There is one deployed binary, one Kubernetes deployment, one CI pipeline — but internally the code is partitioned into domains that are not allowed to call each other directly.
The 7Mind engineering team describes this as “delightfully boring”: the complexity budget goes into product problems, not distributed systems plumbing.
Domain list
Each domain lives at lib/backend/<domain>/ in elixir-backend:
| Domain | Responsibility |
|---|---|
access | Authorization and access control |
activation | User onboarding workflows |
analytics | Event forwarding to Rudderstack |
content | CMS content delivery, Algolia search, Provider pattern for dual-brand |
external | Thin HTTP wrappers around third-party services (Braze, Aury, Rudderstack) |
monetization | Subscriptions and billing (Chargebee, Fastspring, Barmer, Mondia) |
practice | Session tracking, streaks, progress, history |
prevention | ZPP insurance certificates (PDF generation, Cloud Storage) |
shared | Event bus, observability, HTTP pools, config helpers |
user_identity | Authentication, sessions, SSO (Apple, Google, Facebook), SuperTokens |
web | Phoenix endpoints and routers only — no business logic lives here |
How domains communicate
Domains must not import from each other directly. Two allowed coupling mechanisms:
- Event bus (primary) — domain publishes a struct to Google Pub/Sub; other domains or external consumers subscribe via Broadway. See backend-event-bus.
- Explicit public interface — a module exposes a narrow public API surface; other domains call only that surface, never internal modules.
Direct cross-domain function calls to internal modules are a convention violation caught in code review.
Per-domain databases
Each domain has its own PostgreSQL database and its own Ecto repo. They do not share tables. Environment variables follow the pattern BACKEND_<DOMAIN>_DATABASE_URL.
Known domain databases:
BACKEND_PREVENTION_DATABASE_URLBACKEND_PRACTICE_DATABASE_URLBACKEND_MONETIZATION_DATABASE_URLBACKEND_USER_IDENTITY_IDENTITY_DATABASE_URLBACKEND_USER_IDENTITY_APIV1_DATABASE_URLBACKEND_MICROPAYMENTS_DATABASE_URL
Migrations are domain-scoped and run per-database. The Kubernetes init container runs all migrations before the main container starts.
Why this matters for agents
- When editing code in one domain, do not reach into another domain’s internals. Find the public interface or publish an event.
- Migrations belong to their domain. Never write a migration that joins or alters another domain’s tables.
- Adding a new external service dependency? It goes in the
externaldomain first as a thin wrapper, then consumed by the domain that needs it.
Related
- backend-event-bus — how events cross domain boundaries asynchronously
- dual-brand-routing — how the same domain model serves both 7Mind and 7Sleep
- elixir-backend — the repo this pattern lives in