How do logical boundaries (modules, bounded contexts) relate to physical deployable units (processes, services)? Does every bounded context need its own service?
answer
- logical = who may know whom; physical = what ships alone
- split for cadence, scaling, isolation, security, team autonomy
- big ball of mud vs distributed monolith
- shared database ⇒ not independent
- modular monolith keeps the option; extract on evidence
basics
~20 sNo. A logical boundary says who may depend on whom; a deployable unit says what ships and runs separately. You can have strong logical boundaries inside one deployable process (a modular monolith) and only split into services when you need independent deployment, scaling, or isolation.
solid answer
~60 sLogical boundaries are about knowledge and dependency — which code may know about which, and through what contract. Physical boundaries are about deployment and runtime — what builds, ships, scales, and fails independently. They are separate decisions, and the physical one should be driven by requirements you cannot satisfy in-process: independent release cadence, independent scaling of a hot component, fault or resource isolation, separate security/compliance domains, differing technology needs, or team autonomy at scale (Conway's law). Splitting without those drivers buys network latency, partial failure, distributed transactions, versioned contracts, and operational overhead for nothing. The failure modes are symmetric: a *big ball of mud* is one deployable with no logical boundaries; a *distributed monolith* is many deployables whose logical boundaries are wrong, so they must be released together and every request fans out synchronously. The sane default is a modular monolith with rigorously enforced logical boundaries — each module owning its data, no cross-module transactions, coarse contracts — which keeps the option to promote a module to a service cheaply once a real driver appears. Data ownership, not code layout, is the decisive constraint: a shared database silently re-couples 'separate' services.
go deeper
Say a module is a code-level separation and a service is something you deploy separately, and that not every module needs to be a service.
Give the drivers for splitting (deploy cadence, scaling, isolation) and the costs (latency, partial failure, versioning), and name the distributed monolith.
Argue the default modular monolith, data ownership as the real boundary, promotability criteria, and evidence-driven extraction; connect to Conway's law.
Treat the split as an irreversible-ish organizational commitment: weigh migration cost of a wrong boundary, team topology and on-call, contract governance, and sequencing of extraction against actual measured pressure.
## Two different questions 1. **Logical (design-time):** what are the parts, what does each hide, who may depend on whom, through what contract? Units: module, package, component, **bounded context**. 2. **Physical (deploy-time/runtime):** what is built, versioned, released, scaled, and can fail independently? Units: process, container, service, function, deployable artifact. They are related but not identical. One deployable can contain many strong logical boundaries. One logical context can, in rare cases, be split across deployables (e.g. a read-side replica). The common mistake is treating 'we drew a box on the diagram' as 'we must deploy a service'. ## Vocabulary - **Bounded context** (DDD): the scope within which a particular model and its ubiquitous language are consistent. 'Product' means one thing in Catalog and another in Shipping; a bounded context is where one meaning holds. It is *primarily a logical/linguistic boundary.* - **Deployable unit / independently deployable component**: something you can release on its own schedule without coordinating a simultaneous release of others. - **Modular monolith**: one deployable, many enforced modules with private internals and their own data (often separate schemas), communicating through explicit in-process contracts or in-process events. - **Microservices**: logical boundaries promoted to physical ones, each with its own datastore and lifecycle. ## What only a physical split gives you Be explicit about these, because they are the *only* good reasons to pay the price: 1. **Independent deployment / release cadence** — ship a component without re-releasing the whole system; smaller blast radius per release. 2. **Independent scaling** — one component is CPU-hungry or 10× the traffic; scale only it. 3. **Fault isolation** — a memory leak, runaway thread pool, or crash stays inside one process. In a monolith, a resource hog degrades everything sharing the runtime. 4. **Resource/technology heterogeneity** — this piece needs a GPU, a different language, a different runtime version. 5. **Security/compliance isolation** — a component handling card data or secrets in its own trust boundary reduces audit scope. 6. **Team autonomy at scale** — a service per team reduces merge/release contention. **Conway's law**: system structure mirrors communication structure; the *inverse Conway manoeuvre* deliberately shapes teams to get the architecture you want. If none of these apply to a proposed split, keep it logical. ## What a physical split costs - Network latency and partial failure on every crossing (timeouts, retries, idempotency). - Loss of ACID across the boundary → sagas, outbox, eventual consistency, compensations. - Versioned, backward-compatible contracts and a deprecation process. - Distributed tracing, correlation IDs, aggregated logs; harder debugging. - Duplicate infrastructure: pipelines, dashboards, alerts, on-call, secrets. - Refactoring across the boundary becomes a multi-repo, multi-release negotiation — this is the big one. **Moving a boundary is cheap in-process and expensive across services**, which is exactly why you should not distribute boundaries you are not confident about. ## The two failure modes - **Big ball of mud**: one deployable, no enforced logical boundaries. Everything imports everything; no independent reasoning, testing, or replacement. - **Distributed monolith**: many deployables, but the logical boundaries are wrong or nonexistent. Signs: services must be deployed together; one user request synchronously fans out through five services; services share a database or a common 'entities' library that changes constantly; a single feature routinely touches four repos. You pay the full distribution tax and get none of the independence. The distributed monolith is the worse failure, because the fix (moving a boundary) is now expensive. ## Data ownership is the real boundary Code separation without data separation is theatre. If two services read and write the same tables: - either can break the other with a schema change, - neither can be deployed independently in practice, - invariants are enforced in two places or nowhere. So the rule is **one owner per piece of data**; others get it through the owner's API or through published events/read models they maintain themselves. In a modular monolith the analogous discipline is per-module schemas, no cross-module joins, no foreign keys across module boundaries, and no cross-module database transactions. Adopting that discipline early is precisely what makes a later split feasible. ## Promoting a boundary A logical boundary is *promotable* when: it owns its data; its contract is coarse and asynchronous-tolerant; there are no cross-boundary transactions; the two sides don't share mutable state or fine-grained types; and its dependency direction is one-way. If you maintain those properties, extracting a service later is mostly plumbing — routing, serialization, and operations — rather than redesign. The pragmatic sequencing many teams use: start modular-monolith, enforce boundaries mechanically, watch which module actually develops a distinct scaling profile, release cadence, or failure need, and extract *that one*. Extraction driven by evidence beats up-front decomposition, because early domain understanding is at its weakest exactly when up-front decomposition demands it be strongest. ## Nuances worth mentioning - Multiple contexts can share one deployable *and* the same team; that's normal. - One context occasionally spans deployables (a heavy async worker for the same context, or a CQRS read model). Contexts and processes are not required to be 1:1 in either direction. - 'Serverless function per endpoint' is a physical decomposition that can be entirely orthogonal to logical boundaries; the same discipline applies. - Monorepo vs. polyrepo is yet a third, independent axis — repository layout is not architecture.
- Two services have separate codebases and pipelines but share one database schema. Are they independently deployable?No. A schema change by either can break the other, so releases must be coordinated and neither can evolve its storage freely. Shared data means shared lifecycle — that's a distributed monolith with extra network hops.
- What concrete signal would justify extracting one module from a modular monolith into its own service?Evidence, not aesthetics: that module drives most of the CPU/memory and needs a different scaling curve; it needs to release far more often than the rest; its failures repeatedly take down unrelated functionality; it carries compliance-sensitive data you want isolated; or a dedicated team is now blocked by release contention.
- Which properties make a module cheap to extract later?It owns its data with no cross-module joins or transactions; its contract is coarse and tolerates asynchrony; dependencies point one way; no shared mutable state; and its public surface is small and expressed in its own types.
Rooms in a house versus separate buildings. Walls give you privacy and independent use cheaply; you can move a wall in a weekend. Separate buildings give real isolation — a fire in one doesn't spread — but now you need roads, utilities and permits for each, and moving a wall means moving a building.