Components, Interfaces, and Boundaries
By the end of this module you will be able to:
- Define components, interfaces, and boundaries in architectural terms
- Apply cohesion and coupling metrics to evaluate a component design
- Draw boundaries that encapsulate change and limit blast radius
- Explain Postel's robustness principle and why it protects interface stability
- Identify the coupling type present in a shared-database design
With the learning outcomes established, this module begins by examining what a component is in depth.
2.1 What a component is
Every software system, from a two-line script to a distributed platform spanning multiple continents, is composed of components that communicate through interfaces. Getting this composition right is the foundation of all architectural work. Get it wrong and a change in one place triggers cascading failures across the system. Get it right and components can be replaced, scaled, and tested in isolation.
A component is a software element that encapsulates a set of related functionality and exposes it through a defined interface. A component has a clear responsibility, hides its internal implementation, and communicates with other components only through its interface. Examples: a Payment Service, a PDF Generator, an Authentication Module, a Notification Queue.
Components can be large or small: a microservice, a library, a module within a monolith, or a function in a functional pipeline. What makes something a component is not its size but its properties: a defined responsibility, a hidden implementation, and an explicit interface through which all interaction happens.
Poorly defined component: UserService that handles users, orders, and billing. This has low cohesion because it has multiple unrelated reasons to change. When billing logic needs updating, the user authentication code must be retested even though nothing changed.
Better alternative: split into UserAccount (authentication, profile, preferences), OrderManager (order lifecycle, status, history), and BillingService (invoices, payment methods, charges). Each has a single reason to change.
Another poor component: Utils, a catch-all for miscellaneous helper functions. No clear responsibility means no clear ownership, no clear interface, and no clear reason to keep things together or apart. Group utility functions by domain instead: StringUtils, DateUtils, CurrencyUtils.
With an understanding of what a component is in place, the discussion can now turn to interfaces as contracts, which builds directly on these foundations.
2.2 Interfaces as contracts
An interface is the contract between a component and its callers: the set of operations the component offers, the data types those operations accept and return, the error conditions they can produce, and any behavioural constraints such as ordering or concurrency. An interface is a promise about behaviour, not an implementation.
This distinction matters enormously. A component can change its internal implementation completely, using a different algorithm, a different library, even a different programming language, without breaking any of its callers, as long as the interface contract remains stable. When the implementation leaks through the interface, that protection disappears.
“Be conservative in what you do, be liberal in what you accept from others.”
Jon Postel (1980) - RFC 761: Transmission Control Protocol, Section 2.10
Postel's robustness principle, written for TCP/IP, applies directly to interface design. An interface should accept a wide range of valid inputs without failing on minor variations, but produce outputs that strictly conform to the contract. This makes interfaces resilient to caller evolution while remaining predictable for downstream components.
A well-designed interface is minimal: it exposes only what callers need, not the full surface area of internal behaviour. It is stable: it changes as infrequently as possible, because every change breaks every caller. It is explicit: it documents what inputs are valid, what outputs mean, and what errors can occur. And it is technology-agnostic: a REST API, a gRPC service, and a TypeScript function with the same semantic contract are equally valid interfaces.
A poor interface leaks implementation. A UserService interface that exposes getUserFromDatabase(sql: string) forces callers to know they are talking to a database and to know SQL. The moment you want to add caching or switch to a different data source, you have to change every caller. A better interface exposes findById(userId: string) and hides the rest.
With an understanding of interfaces as contracts in place, the discussion can now turn to where to draw boundaries, which builds directly on these foundations.
2.3 Where to draw boundaries
An architectural boundary is a line of demarcation between two components or groups of components. Boundaries define what belongs inside a unit and what must cross the boundary to reach another. Boundaries limit the spread of change: a modification inside a boundary should not require modifications outside it.
Boundaries should be drawn along four lines. First, different rates of change: things that change together should be inside the same component. Things that change independently should be on opposite sides of a boundary. Second, different deployment needs: if two parts of the system need to be deployed independently, they need a boundary.
Third, different team ownership: Conway's Law, named after Melvin Conway who observed it in 1968, states that systems mirror the communication structure of the teams that build them. If two teams own a shared database table with no boundary between them, the teams will fight over schema changes indefinitely. Fourth, different trust levels: user-provided data versus internally generated data; public APIs versus internal services.
Common misconception
“Small components are always better. Decompose everything into the smallest possible units.”
Over-decomposition creates unnecessary integration overhead. Every component boundary introduces a network call, a serialisation cost, a contract to maintain, and an operational unit to deploy and monitor. Decompose where there is genuine independent variability: different rates of change, different teams, different deployment needs. Do not decompose for its own sake.
With an understanding of where to draw boundaries in place, the discussion can now turn to cohesion and coupling, which builds directly on these foundations.
2.4 Visualising boundaries: tightly coupled vs well-bounded
The two views below show the same five components. On the left, all components connect directly to all others. On the right, they are grouped into bounded contexts with defined interfaces between them. Toggle between views and click a component to see how many dependencies it carries.
With an understanding of visualising boundaries: tightly coupled vs well-bounded in place, the discussion can now turn to cohesion and coupling, which builds directly on these foundations.

2.5 Cohesion and coupling
Two metrics measure the health of component design. Cohesion measures how well the elements within a component belong together. High cohesion means everything in the component has a single, related purpose. Low cohesion means the component handles unrelated concerns and has multiple reasons to change.
Coupling measures the degree of interdependence between components. Low coupling means components can be changed independently. High coupling means a change in one component requires changes in others. The goal is high cohesion and low coupling. These two properties are related: well-drawn boundaries produce both.
Coupling exists on a spectrum. Content coupling is the most damaging: Component A modifies Component B's internal data directly, bypassing B's interface. Direct database access across services is the most common example. Common coupling means components share global mutable state: any component can change the state in ways that affect all others.
Control coupling occurs when Component A passes a flag to Component B that controls B's internal logic flow: for example, processOrder(order, isTestMode). Now A has to know about B's internal branching. Data coupling, the most acceptable form, means A and B communicate only through well-defined parameters, like a REST API with typed request and response bodies. Each component knows only what it needs to know about the other.
“The criteria to be used in decomposing a system into modules: each module should hide a design decision from the rest of the system.”
David Parnas (1972) - On the Criteria To Be Used in Decomposing Systems into Modules. Communications of the ACM, 15(12):1053-1058.
Information hiding is the mechanism that produces low coupling. When a component hides a design decision, changes to that decision stay inside the component. No other component breaks because no other component knew about the decision in the first place. This is why interfaces matter: they are the boundary behind which decisions are hidden.
With an understanding of cohesion and coupling in place, the discussion can now turn to dependency direction and the dependency inversion principle, which builds directly on these foundations.

2.6 Dependency direction and the Dependency Inversion Principle
Every dependency is a coupling. When Component A imports Component B, A depends on B. If B changes its interface, A must change too. The direction of dependencies determines what can change independently and what cannot.
Robert C. Martin's Dependency Inversion Principle, the D in SOLID, states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Concretely: a business logic layer should not import from the database layer. Instead, the business logic defines an interface (a repository abstraction), and the database layer implements it. Now the database layer depends on the business logic, not the other way around.
This inversion means the business logic can be tested without a database, deployed before the database implementation is finished, and connected to a different storage mechanism (in-memory store, document database, event log) without changing the business logic at all. Dependencies flowing toward stable abstractions produce systems that are easier to test, extend, and replace.
Common misconception
“Interfaces just mean function signatures. As long as the function name and parameters match, the interface is intact.”
An interface is the full contract: the function signatures, the data types, the error semantics, the ordering constraints, and the behavioural invariants. A function that accepts the same parameters but now returns a null where it previously raised an error has broken its interface contract even though the signature is unchanged. Consumer-driven contract tests catch this class of break that type systems alone cannot.
A good boundary makes the blast radius of a change predictable. If you change something inside a boundary, you know exactly what you might break: only the things on the same side. If there is no boundary, you cannot know.
Amazon's 2002 API mandate banned direct database links and shared memory access between teams. What architectural principle does this mandate enforce most directly?
A NotificationService handles sending emails, sending SMS, storing notification preferences, querying notification history, and formatting templates. It has 12 database tables and 40 API endpoints. What is the primary architectural problem?
Service A queries Service B's database tables directly, reading and writing rows without going through Service B's API. Which coupling type is this, and why is it the most harmful?
Key takeaways
- Components encapsulate related functionality behind an interface. The interface is the contract; the implementation is the component's private concern.
- Good interfaces are minimal, stable, explicit, and technology-agnostic. Postel's robustness principle: be liberal in what you accept, strict in what you produce.
- Boundaries should be drawn along lines of different rates of change, different deployment needs, different team ownership, and different trust levels.
- High cohesion (a component does one thing) and low coupling (components are independent) are the two measures of sound component design.
- Content coupling, where one component reaches into another's internal data, is the most damaging coupling type and the most common cause of cascade failures in distributed systems.
- Dependency direction matters: dependencies should flow toward stable abstractions, not toward concrete implementations.
Standards and sources cited in this module
The foundational paper on information hiding. Every modern principle about interfaces and component boundaries descends from this work. Quoted in Sections 2.5 and 2.6.
Martin, R.C. (2017). Clean Architecture. Prentice Hall
Part III: Design Principles; Part IV: Component Principles
Core reference for component boundaries, dependency rules, and the stable abstractions principle. The Dependency Inversion Principle in Section 2.6 comes from this source.
Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley
Part IV: Strategic Design
Defines bounded contexts as the primary tool for boundary placement in complex business domains. The bounded context view in the Component Boundary Explorer is grounded in this concept.
Postel, J. (1980). RFC 761: Transmission Control Protocol. Internet Engineering Task Force.
Section 2.10: Robustness Principle
The original source of Postel's robustness principle quoted in Section 2.2. Directly applicable to interface design in any protocol or API.
Richardson, C. Microservices Patterns. microservices.io
Chapter 2: Decomposition strategies
Practical guidance on applying bounded contexts and decomposition patterns to service boundaries. Extends the theory in Section 2.3 with concrete decomposition heuristics.
What comes next: Components and boundaries give structure. But how do you know whether the structure is good enough? Module 3 introduces quality attributes - the measurable properties like performance, reliability, and security that determine whether an architecture meets its goals, not just whether it compiles.
Module 2 of 22 in Foundations