Rereading Clean Architecture (6): Boundaries

Tommy Cheese | · Reading time: 3 min

What Are Boundaries?

Software architecture is the art of drawing boundaries.

Boundaries divide software into elements and constrain dependencies across their two sides. Drawing boundaries early in a project allows decisions to be deferred, ensuring that those decisions will not later interfere with the system’s core business logic. Boundaries also reduce or eliminate unnecessary architectural coupling. Coupling—especially coupling caused by premature, immature decisions about frameworks, databases, and other details that should have been postponed—is what consumes the most human effort in a system.

How should we draw boundaries? Two basic principles help:

  • Place boundaries between unrelated concerns, such as the GUI and business logic. Input and output through a GUI are incidental to the business logic, which should support different GUI implementations.
  • Draw boundaries along the axes of change in a system. Components on opposite sides should change for different reasons and at different rates. The principles that guide these axes are SRP at the module or class level and CCP at the component level.

Having established why boundaries matter and where they belong, we can consider the basic process of drawing them:

  1. First divide the system into components. Some contain core business logic; others are plugins that provide necessary functionality unrelated to that core.
  2. Modify the source code so that these noncore components depend on the system’s core business logic components.

Drawing boundaries is a concrete application of dependency inversion and stable abstractions. Dependency arrows should point from low-level concrete implementations toward high-level abstractions.

Anatomy of Boundaries

A system’s architecture is defined by its software components and the boundaries between them

Boundaries take several forms. This chapter examines them through calls that cross boundaries.

At runtime, crossing a boundary means a function on one side calls a function on the other and passes data to it. Designing these calls properly requires careful control of source-code dependencies. Without that control, changing one module’s source may force other modules to change or be recompiled and redeployed. Clear boundaries help reduce this effect.

Cross-boundary calls occur at several levels: source code, deployment, services, and physical boundaries.

Crossing Boundaries at the Source-Code Level

Source-code boundaries appear in monolithic architectures. Such architectures generally need some form of dynamic polymorphism to manage internal dependencies.

The simplest cross-boundary call occurs when a low-level client calls a high-level service function. Runtime and compile-time dependencies then point in the same direction, from the low-level component to the high-level component.

(Original diagram unavailable)

When a client in a high-level component needs to call a service in a low-level component, dynamic polymorphism is needed to invert the dependency. The Service interface in the diagram is a form of SPI.

(Original diagram unavailable)

Crossing Boundaries at the Deployment Level

At the deployment level, deployable units are packaged into a convenient file format. Apart from this distinction, components decoupled at deployment boundaries are nearly identical to a monolithic structure.

Deployment boundaries involve physical boundaries because separately deployed units must communicate across them. Common physical boundaries include dynamically linked libraries, threads, and local processes.

Crossing Boundaries at the Service Level

Services form the strongest architectural boundaries. A service is a process. When defining architectural boundaries at this level, minimize communication wherever possible, and ensure that it can tolerate high latency. Otherwise, the same rules used for local processes apply: lower-level services should act as plugins for higher-level services.

comments powered by Disqus