DEV Community

Remo H. Jansen
Remo H. Jansen

Posted on

The Boundary Independence Principle (BIP)

I've been writing about the SOLID principles for over a decade. I built InversifyJS because of them, I wrote about implementing them with the onion architecture, and just recently I argued that they are universal design principles that appear far beyond object-oriented programming. Yet something has always felt missing, not from the principles themselves, but from the conversation around them.

SOLID tells you how to write good components. It does not tell you how to compose those components into a system whose shape can still change.


The gap

Imagine a perfectly SOLID codebase. Your UserRepository depends on an abstraction. Your EmailService has a single responsibility. Your OrderProcessor is open for extension and closed for modification. Everything is clean.

Then your CTO says: "We need to extract user management into its own microservice."

You begin ripping the system apart. New projects, moved files, rewritten imports, new entry points, new dependency wiring, shared interfaces extracted into packages. Weeks of work follow. The SOLID components themselves needed almost no change. The structure around them did. The boundaries of the system had been baked into import paths, project layout, entry points, and hard-coded module wiring.

SOLID gave you excellent bricks. Without a principle that keeps those bricks from being cemented into a fixed shape too early, you still end up with a rigid system.


A convergence of patterns

Software engineering literature is rich with architectural patterns: Hexagonal Architecture, Onion Architecture, Ports & Adapters, Composition Roots, Modular Monoliths, Microkernels, and plugin systems.

On the surface they address different concerns:

  • Composition Root , assemble the object graph in one place at startup.
  • Ports and Adapters / Hexagonal , isolate domain logic behind explicit interfaces.
  • Onion / Clean Architecture , enforce inward-pointing dependencies toward business rules.
  • Modular Monolith , keep a single deployable while enforcing strict module encapsulation.
  • Microkernels and plugins , keep a minimal core unaware of the extensions that run inside it.

Despite different origins, applying these patterns well repeatedly produces the same emergent property:

You can change the physical shape of a system without altering the code that performs the actual business logic.

When independent patterns converge on the same property, it is worth asking whether they are expressions of a deeper principle.


System shape

System shape is the physical layout, deployment targets, and runtime distribution of a system:

  • One process or many services?
  • In-memory calls or network messages?
  • Monolith container, shared library, or serverless functions?

Shape decisions are among the most expensive an organization can make. Teams that choose microservices too early often spend months consolidating. Teams that stay in a monolith too long often spend a year extracting services. These transitions hurt because business logic frequently carries implicit assumptions about where it runs, shared memory, instantaneous responses, or a single database.

The patterns above make system shape mutable. They allow teams to reconfigure the physical layout without rewriting domain rules.


What changes with the Composition Root

Consider the same codebase wired exclusively through an IoC container, with all wiring living in composition roots.

In a monolith:

// monolith/composition-root.ts
const container = new Container();
container.load(authModule);
container.load(userModule);
container.load(orderModule);
container.load(emailModule);
container.load(cmsModule);
Enter fullscreen mode Exit fullscreen mode

When the organization decides to split services, new composition roots select different subsets:

// services/user-service/composition-root.ts
const container = new Container();
container.load(authModule);
container.load(userModule);
Enter fullscreen mode Exit fullscreen mode
// services/order-service/composition-root.ts
const container = new Container();
container.load(authModule);
container.load(orderModule);
container.load(emailModule);
Enter fullscreen mode Exit fullscreen mode

The components themselves do not change. Only the composition roots change. The boundaries of the system, what constitutes a deployable unit, were never encoded inside the components.

I documented a concrete case study of this approach: From Monolith to Microservices without changing one line of code. The same source produced either a monolith or a set of microservices depending on which composition roots and CI/CD configuration were used. We even swapped CosmosDB for PostgreSQL by replacing one IoC module.


This is not merely the Composition Root pattern

The Composition Root (Mark Seemann) tells you where to wire dependencies: in a single place close to the application’s entry point. Seemann correctly insists that each deployable artifact should have exactly one composition root.

The principle I am naming is different. It is not primarily about where wiring occurs. It is about what components are allowed to know about their own boundaries.

  • The Composition Root pattern is a mechanism.
  • Boundary Independence is the reason to keep components ignorant of shape so that new composition roots can redefine system shape without touching the components.

In large systems the ideal of a single pure composition root often becomes hierarchical or framework-assisted wiring. The principle still holds: the decision about which components belong together must remain outside the components themselves.


The core mechanism: ignorance of boundaries

The consistent mechanism across these patterns is simple:

Components that execute core business logic must remain ignorant of the boundaries that enclose them.

A domain service that processes a customer transaction should not know:

  • whether its storage dependency is an in-memory map, a local database, or a remote API;
  • whether it is running inside a Lambda, an Express process, or a CLI;
  • whether an event it emits is handled synchronously or placed on a distributed broker.

Because the component has no access to infrastructure details and no way to query its execution environment, it stays passive to shape changes. Relocate it from a monolith to a serverless worker and it continues to function.

This is the heart of the idea: components must not encode assumptions about the shape in which they happen to execute.


The Boundary Deferral Spectrum

A useful mental model is a spectrum of how late boundary decisions can be deferred:

Design time (worst)

Boundaries are baked into source structure, import paths, and project layout. Restructuring requires rewriting.

Composition time (good)

Boundaries are defined only in composition roots. Components are boundary-agnostic. Different roots produce different shapes from the same components.

Build time (better)

Composition roots (or equivalent configuration) read build arguments or environment variables. A single codebase yields different deployable artifacts. Restructuring becomes a change of build configuration.

Runtime (most deferred)

The system discovers and loads components dynamically. Plugin architectures (MEF, OSGi, VS Code extensions, browser extensions) do not know their final shape until they are running. The system’s boundaries are defined by what is present at runtime.

Plugin systems are the purest practical embodiment of this spectrum. They are also among the longest-lived and most adaptable software systems we have built, IDEs, browsers, operating systems. They defer boundary decisions until the last responsible moment.


Horizontal and vertical slicing

Boundary decisions come in two flavours; both should be deferrable.

Vertical slices concern technical layers: HTTP, domain logic, persistence. In a well-structured onion or hexagonal architecture these layers are already separated by abstractions. The composition root connects them. Swapping an entire persistence technology becomes a composition-root change.

Horizontal slices concern business capabilities: user management, order processing, content, authentication. In a modular monolith these appear as modules; in a microservices architecture each becomes its own deployable with its own composition root.

Both kinds of boundary should be defined outside the components. Components should not know whether two repositories live in the same process or across a network, or whether an email service is an in-process call or a remote API. Those are composition-time decisions.

When both vertical and horizontal boundaries are deferred, the system becomes a collection of Lego bricks. You can assemble a monolith, a set of services, or anything in between without modifying the bricks, only the instruction manual changes.


The same idea in functional programming

The principle is not limited to OOP or IoC containers.

In ML-family languages, module functors parameterise an entire module over its dependencies. The wiring occurs at the call site; the functor itself remains boundary-agnostic.

Effect systems such as ZIO declare dependencies as a type-level environment. Different layer configurations can be assembled for tests, a monolith, or distributed deployment. The program does not change, only the layer composition does.

Even simple higher-order functions achieve the same effect: a function that receives its collaborators as parameters does not know whether those collaborators are local, remote, or mocked.

The mechanisms differ; the principle is identical.


Boundary Independence and Dependency Inversion

A natural question is whether this is simply Dependency Inversion applied at larger scale.

There is a close relationship, but they are not the same.

  • Dependency Inversion makes a component independent of the concrete implementation of a dependency.
  • Boundary Independence makes a component independent of the shape in which that dependency is fulfilled.

DIP frees you from a particular database driver or HTTP client. BIP frees you from the assumption that the dependency lives in the same process, the same memory space, or the same failure domain. BIP builds on DIP and extends the reasoning from class and module boundaries to architectural and physical boundaries.


Architectural trade-offs and costs

Flexibility always has costs. Making components shape-independent introduces real challenges that must be managed deliberately.

1. Contract design for multiple topologies

Interfaces must express guarantees that remain valid whether the dependency is local or remote. Assumptions about shared memory, instantaneous response, or transactional consistency across components cannot be hidden inside domain logic if the boundary may later move across a network.

2. Translation and mapping overhead

Data crossing a boundary usually requires mapping between transport formats and domain models. This adds code and a modest performance cost compared with passing raw persistence entities through the application.

3. Latency and performance blindness

When code is unaware of boundaries, developers can write sequential chatty interactions that are acceptable in-process but catastrophic across a network. Boundary Independence does not remove the need for performance thinking; it requires that the costs of each shape be made explicit at the boundary (bulk APIs, async messaging, caching strategies, etc.).

4. Consistency, failure, and observability

Network boundaries introduce partial failure, different consistency models, and the need for distributed tracing, circuit breakers, and clear ownership of data. These concerns cannot be fully abstracted away. They must be addressed at the edges while the domain logic remains ignorant of them.

Boundary Independence does not claim that shape is irrelevant. It claims that shape should remain a replaceable architectural decision, while the semantics and costs of each shape are handled explicitly at the boundary.


Has this been said before?

Yes, but scattered across different communities and disciplines.

The practice is not new. Strong teams have been building boundary-agnostic components and deciding system shape at composition time for years. What has been missing is a concise, named principle that sits cleanly alongside SOLID and explicitly connects the dots.

The existing sources

These concepts and patterns already exist:

  • Robert C. Martin (Clean Micro-service Architecture): “The Deployment Model is a Detail,” “Forced Ignorance”
  • Mark Seemann (Dependency Injection pattern): The Composition Root mechanism; exactly one composition root per deployable artifact
  • The Lean community: “Decide as Late as Possible” — defer irreversible decisions until the last responsible moment
  • Domain-Driven Design & Hexagonal/Onion Architecture: Isolate domain logic behind abstraction boundaries
  • Plugin architectures (VS Code, browsers, operating systems): Runtime deferral of component identity and shape
  • Modular Monoliths & Modular Monolith Architecture: Treat internal boundaries as if they might become external later

These are not new ideas. This article is not inventing them.

What's missing: the synthesis

However, these sources treat the idea as separate concerns:

  • Uncle Bob writes about deployment models and clean architecture, but still largely assumes component boundaries are fixed in project or package structure.
  • The Composition Root pattern tells you where to wire, but not that components must be deliberately designed to be boundary-agnostic.
  • “Decide as Late as Possible” is a general heuristic, not a specific principle about system boundaries.
  • Plugin architectures demonstrate the idea perfectly, but are often treated as a specialist domain (browser extensions, IDEs) rather than a principle that applies to all systems.

None of these sources state the full, unified claim:

The boundaries of a system — which components are grouped together, what constitutes a vertical or horizontal slice, what constitutes a deployable unit — should be defined outside the components themselves (normally at composition-root level or later) and should be reconfigurable without modifying the components.

Why name it?

By naming the principle and showing its convergence across patterns, we can:

  1. Make it teachable — rather than learning Composition Root, Hexagonal Architecture, and modular monoliths as separate ideas, teams can learn one unifying principle and recognize it in all of them.

  2. Enable deliberate design — teams can ask “Are we applying BIP?” during architecture and code review, with a shared language.

  3. Connect it to SOLID — BIP is the architectural answer to a question SOLID leaves open: “Once I have excellent components, how do I ensure the system's shape remains flexible?” Naming it makes that connection explicit.

  4. Prevent accidental commitment — when teams see that plugin architectures, successful modular monoliths, and successful microservices migrations all rely on the same underlying principle, they can avoid encoding shape decisions inside domain logic by default, not by luck.


The principle

I am naming the existing, convergent practice:

The Boundary Independence Principle (BIP)

Components should declare what they require to operate, while remaining ignorant of how, where, or when those requirements are fulfilled across system boundaries. System boundaries, both vertical (technical layers) and horizontal (business capabilities), should be defined at the composition-root level (or later), never inside the components themselves. Boundary decisions should be deferred as late as practically possible.

More concisely: system boundaries are independent of the components themselves.

This extends the familiar framing — database is a detail, framework is a detail, deployment model is a detail — to its logical conclusion: the boundaries themselves are a detail too.


Intellectual lineage

This principle is built on decades of work by many practitioners:

  • Alistair Cockburn — Hexagonal Architecture / Ports & Adapters
  • Robert C. Martin — Clean Architecture, dependency inversion, deployment as a detail
  • Mark Seemann — Dependency Injection patterns and the Composition Root
  • Domain-Driven Design community — Entity, aggregate, and bounded context boundaries
  • Lean software development — Defer decisions until the last responsible moment
  • Plugin architecture patterns — Used successfully in OS kernels, browsers, IDEs, and application platforms for decades

The contribution of naming Boundary Independence is not inventing a new idea. It is making explicit what these patterns converge on, giving it a place alongside SOLID, and enabling teams to apply it deliberately rather than accidentally.


Practical application

Naming the principle gives teams a shared heuristic for design and review:

  1. Architecture and code reviews

    Ask: “Does this domain logic contain assumptions about its execution boundary?” Importing infrastructure SDKs, assuming shared transactions, or hard-wiring module relationships inside business logic are boundary leaks.

  2. Deferring expensive decisions

    Start with a modular monolith whose internal boundaries are already BIP-compliant. Extract services later only when metrics justify the operational cost. The extraction cost drops dramatically because the components were never entangled with the old shape.

  3. Testing

    Domain logic that is oblivious to its runtime environment can be tested with lightweight in-memory adapters. This simplifies unit and many integration tests. It does not eliminate the need for contract tests, end-to-end tests, or chaos experiments once real distribution appears; those remain necessary at the boundaries.

The goal is not to make every system ready to become a microservice architecture on day one. The goal is to avoid accidentally encoding shape decisions inside the components that should not have to care.


SOLID + BIP

SOLID without Boundary Independence has always felt incomplete. SOLID gives you high-quality components. Without a principle that keeps boundaries deferred and external to those components, teams still cement the bricks into a shape too early. When the shape must change, and it almost always must, the cost is far higher than it needed to be.

The teams that extract the most value from SOLID are the ones that, whether they name it or not, also practise Boundary Independence. Their systems behave like plugin architectures even when they are not formally plugins. Components do not know their own shape. Boundaries live in one place (or a small number of controlled places). Changing that place changes the shape of the system.

SOLID gives us the bricks. BIP keeps the mortar from setting too soon.

I would love to hear your thoughts.

Top comments (0)