[Book Study] Clean Architecture — Part 5

Zong Yuan
7 min readDec 16, 2021

This article covers boundaries section.

Boundaries: Drawing Lines

Software architecture is the art of drawing lines that call boundaries.

Boundaries separate software elements from one another, restrict those on one side from knowing about those on the other.

Boundaries might be drawn at early stage for the purposes of deferring decisions for as long as possible, and of keeping those decisions from polluting the core business logic.

A good system architecture prevent system coupled to premature decisions.

Premature decisions are those decisions that have nothing to do with the business requirements.

  • Including frameworks, databases, web servers, utility libraries, dependency injections…

A good system architecture allows these decisions to be made at the latest possible moment, without significant impact.

There are two stories explained in the book about why defer premature decisions are better for development. You should read them especially the second story, it provided a good example for deferred decision.

You can always use interface to draw the boundary lines.

Which Lines Do You Draw, And When Do You Draw Them?

Draw lines between things that matter and things that don’t.

  • GUI vs. Business Rule
  • Database vs. GUI
  • Database vs. Business Rule

The database is NOT the embodiment of the business rules, it is a tool that the business rules can use indirectly. Business rules need to know only there is a set of functions that can be used to fetch or save data.

Business Rules doesn’t know about Database

The Database component contains the code that translates the calls made by the BusinessRule into the query language of the database. It is that translation code that knows about the BusinessRule.

With this boundary line between BusinessRules and Database, and with the direction of the arrow toward the BusinessRules, the Database component could be replaced with different implementations easily — since the BusinessRule don’t care.

What About Input and Output?

The IO is irrelevant.

The Boundary Between GUI and BusinessRules Components

Same as Database, with same boundary lines and the direction of the arrow, the GUI could be replaced with any other kind of interface — the BusinessRule still don’t care.

Plugin Architecture

GUI & Database as plugins to Business Rules

We can view the Database and GUI as the plugins of Business Rules. And from the history of software development technology, we know that plugin architecture helps to establish scalable and maintainable system architecture.

The core business rules are kept separate from, and independent of, those components that are either optional or that can be implemented in many different forms.

Imagine GUI is a plugin, we can always plug in many different kinds of user interfaces. The efforts to change a plugin will be minimal.

Same with database as a plugin, we can always switch SQL databases to NOSQL database with minimum efforts.

The Plugin Argument

We want certain modules to be immune to other modules’ changes. We don’t want changes in one part of the system to cause other unrelated parts of the system to break.

By using plugin architecture, we create firewalls to prevent changes propagated.

  • If the GUI plugs in to the business rules, then changes in the GUI cannot affect those business rules

Draw boundaries according to axis of change.
The components on one side of the boundary change at different rates, and for different reasons, than the components on the other side of the boundary.

  • GUI vs. business rules
  • Business rules vs. framework

The SRP tells us where to draw boundaries.

Conclusion

  1. Partition the system into components
  2. Identify core business rules components and other plugin components
  3. Arrange the code in those plugin components point toward the core business rules components

Boundary is an application of the Dependency Inversion Principle and the Stable Abstraction Principle.

  • lower-level details point to higher-level abstractions

DIP: for example, you control your dependencies direction between Business Rules and Database. By using interfaces, Business Rules only know the interface will provide the functions they required, but not aware or be affected by the implementation of those functions.

SAP: higher-level business rules is stable components, as the stability definition here is the efforts required to make changes. That’s why they should be abstract so they are easier to be extended.

Boundary Anatomy

The architecture of a system is defined by a set of software components and the boundaries that separate them.

Boundary Crossing

A function on one side of the boundary calling a function on the other side and passing a long some data.

The trick to creating an appropriate boundary crossing is to manage the source code dependencies.

Following sections explained different levels of boundary strategy and their boundary crossing.

The Dread Monolith

The monolith here can be a monolith application or the service of microservices architecture application.

From a deployment point of view, the monolith is a single executable file.

  • No strict physical representation
  • Disciplined segregation of functions and data that run within same processor and address space
  • Source-level decoupling mode
  • statically linked C/C++ project, set of Java class files bound into an executable jar file, set of .NET binaries bound into a single .EXE file…

The boundary of monolith are not visible during the deployment, but the boundary still important for the ability to independently develop.

This type of architectures always depend on some kind of dynamic polymorphism to manage their internal dependencies.

  • One of the reason OO become such an important paradigm in recent decades.

Simplest boundary crossing is a function call from a low-level client to a higher-level service. Both the run time dependency and the compile-time dependency point in the same direction, toward the higher-level component.

Below shows low-level Client is calling function on higher-level Service with an instance of Data.

Client call function on the Service, along with instance of Data. Note that Data located on the called side of the boundary (higher-level side)

Note that the Data definition is located on the higher-level side.

Data definition need to put on higher-level side so when higher-level Service uses the Data, the direction of dependency still correct.

However, sometimes we might need function call from a higher-level client to a lower-level service. Dependency inversion will be used in this boundary crossing. The runtime dependency will oppose with the compile-time dependency.

Below shows high-level Client is calling function on lower-level Service with an instance of Data. Notice that dependency inversion has been used in this case to prevent the compile-time dependency point toward the low-level Service component.

Notice that high-level client calling interface that located on the high-level side. Data definition also located on the high-level side

Remember that the definitions of Service Interface and Data should located in the high-level component.

Even in a monolithic, statically linked executable, the boundaries still provide great benefits.

  • Independent development
  • Facilitate the testing and deployment
  • High-level components remain independent of the lower-level details

Communication between components in monolith is just function calls, which are fast and inexpensive.

Deployment Components

Deployment level of architectural boundary is similar with monolith, with minor differences:

  • Dynamically linked library such as .NET DLL, Java jar file, Ruby Gem or a UNIX shared library.
  • Deployment does not involve compilation, the components are delivered in binary or some sort of deployable form
  • Deployment-level decoupling mode

It might have a one-time hit for dynamic linking or runtime loading, but the communication across boundaries is fast and inexpensive.

Threads

Threads are not architectural boundaries or units of deployment, but rather a way to organize the schedule and order of execution.

  • Both monoliths and deployment components can make use of threads
  • May be contained within a component or spread across many components

Local Processes

Local processes run in the same processor, or in the same set of processors within a multicore. However, local processes didn’t share address spaces, each of them use a separate address space.

Shared memory can be used to achieve sharing address space between processes, but it requires extra setup.

  • Communication methods
    - sockets
    - mailboxes
    - message queues
  • Each local process can be a monolith or deployment components mentioned above
  • The process consists of lower-level components that manage their dependencies through dynamic polymorphism
  • Source code dependencies should always point toward the higher-level component
  • Communication between local processes is expensive, which might involve
    - Operating system calls (syscall)
    - Data marshaling and decoding
    - Inter-process context switches

I think communicate using API (localhost) is one of the communication methods too

Services

The strongest boundary is a service. Services may or may not operate in the same physical machine, therefore all communications between services are take place over the network.

Communication across service boundaries are very slow and expensive. Communication latencies have to be considered.

Services are similar with local processes, just the communications are different. Beside that, since higher-level services shouldn’t contain details of lower-level service, so the specific physical knowledge such as URI of service shouldn’t be mentioned or written in higher-level source code.

You can consider to use config server to hold the details of services’ physical knowledge, so every services just go get the configuration from the config server. Another way is use a services registry service, the service will auto redirect the request to correct service. jhipster-registry is one of the registry service.

Conclusion

This section explains about drawing boundary lines between components, and introduces several boundary strategies and their boundary crossing. Boundary is important because software components and the boundaries that separate those components are the architecture of a system.

--

--