Skip to content

Service & Component Boundaries (Coupling & Cohesion)

A boundary decides what belongs together (high cohesion) and keeps what’s on either side loosely dependent (low coupling) — get it wrong and every team is blocked on every other team’s deploy.

  • What it is: drawing lines around related functionality (a “bounded context”) and minimizing the dependencies that cross that line.
  • Why it matters: badly drawn boundaries force constant cross-team coordination; well-drawn ones let teams change and deploy independently.
  • For developers: before adding a cross-module call, ask if it crosses a boundary — consider an interface or event instead of reaching directly into another module’s internals.
  • When to use it: whenever splitting a system into services/modules (see Modular Design & Decomposition), or diagnosing why two teams keep breaking each other’s code.
  • When not to use it: don’t draw a hard service boundary (with network calls) just for the sake of it — a module boundary within one deployable is often enough.
  • Prerequisites: Modular Design & Decomposition.
flowchart TB
    subgraph CartB[Cart boundary]
      C1[Cart items]
      C2[Cart totals]
    end
    subgraph PricingB[Pricing boundary]
      P1[Discount rules]
      P2[Tax rules]
    end
    CartB -- defined interface only --> PricingB

Inside each box, everything is closely related (high cohesion). Between boxes, the only connection is a defined interface (low coupling) — never direct access to internals.

  1. Identify a candidate boundary by grouping logic that changes together and is owned by the same team.
  2. Check cohesion: does everything inside the boundary genuinely relate to one business capability?
  3. Check coupling: list every place outside the boundary that reaches into its internals directly (not through an interface) — those are boundary violations.
  4. Replace direct internal access with an explicit interface (a method call, API, or event).
  5. Re-evaluate the boundary as the system evolves — a boundary that requires two teams to coordinate on every change is drawn in the wrong place.

After splitting Checkout into Cart, Pricing, and Payment modules, Pricing still reads Cart’s internal item list directly to compute totals.

  • Input: the Pricing module reaches directly into Cart’s internal data structures whenever it needs item details.
  • Process: the team defines a PricingInput interface (item ids, quantities, customer tier) that Cart passes to Pricing; Pricing no longer touches Cart’s internals.
  • Output: Cart and Pricing can now change independently — Cart can restructure its internal storage without breaking Pricing, as long as it still produces the same PricingInput shape:
// The only thing Pricing is allowed to depend on — Cart's internals stay private
public sealed record PricingInput(
IReadOnlyList<(string Sku, int Quantity)> Items,
string CustomerTier);
public interface IPricingModule
{
Task<PriceQuote> QuoteAsync(PricingInput input);
}
// Cart maps its own internal model to the shared contract, nothing more
PricingInput ToPricingInput(CartState cart) =>
new(cart.Lines.Select(l => (l.Sku, l.Quantity)).ToList(), cart.CustomerTier);
Use case When to use Notes
Two teams keep breaking each other’s code When ownership is unclear Redraw the boundary along team/business-capability lines
Splitting a monolith into services Choosing what becomes its own service Group by bounded context, not by technical layer (e.g. not “all validation code”)
A “utility” module used by everyone When a shared module becomes a bottleneck Consider whether it hides multiple unrelated concerns that should be separate boundaries
  • Bounded context (from Domain-Driven Design): a boundary within which a term means one specific thing — e.g. “Order” means something different to Cart than to Fulfillment.
  • Coupling is not binary: even loosely coupled boundaries share something (a contract); the goal is minimizing and making that sharing explicit, not eliminating it.
  • Conway’s Law: system boundaries tend to mirror team communication structures — misaligned boundaries often point to a team-structure problem, not just a code problem.
  • Use the C4 model to draw boundaries at the right zoom level: a single diagram trying to show both “how does Checkout fit into the wider business” and “what classes exist inside Payment” satisfies no one. C4 names four levels — Context (the system and its users/external systems), Container (deployable units like Cart/Pricing/Payment), Component (the pieces inside one container), and Code — so a boundary discussion can stay explicit about which level it’s actually happening at.
  • Polyglot persistence per boundary: because internals stay hidden behind the interface, each bounded context can pick its own data store — e.g. Payment’s ledger in Azure SQL Database (relational, ACID transactions), Cart in Azure Cosmos DB (flexible schema, low-latency reads at scale), and receipt attachments in an Azure Storage Account (blob storage) — without any other module needing to know or care which store sits behind the interface.
Term Meaning
Bounded context A boundary within which a term/concept has one consistent meaning
Coupling How much one component depends on another’s internals
Cohesion How closely related the things inside one boundary are
Conway’s Law System structure tends to mirror the organization’s communication structure
C4 model Four diagramming levels — Context, Container, Component, Code — for showing structure at the right zoom level
Symptom Likely cause Fix
Every change requires updating two teams’ code Boundary crosses a natural team/business line Redraw the boundary to align with ownership
Same term means different things in different code paths No bounded context defined Introduce an explicit bounded context per meaning
A module reaches into another’s internal data structures No defined interface exists Introduce and enforce an explicit interface
  • What’s the difference between cohesion and coupling?
  • Find a place in a system you know where one module reads another’s internals directly — what interface could replace that?

Part of Introduction to Software & Application Architecture. See also Modular Design & Decomposition, API Design & Contracts, Event-Driven & Asynchronous Communication, and the Glossary.