Choose your own architecture

Choose your own architecture #

Now that we’ve seen patterns decomposed into decoupling and cohesion, we can try reconstructing architecture based on your project’s needs.

Project size #

The project’s expected size is among the main determinants of the project’s architecture as both overgrown components and excessive fragmentation handicap development and maintenance. A moderate number of components of moderate size is the desired zone of comfort.

Therefore, a one day task will likely be monolithic, a man-month of work needs layering while anything larger calls for at least partial separation into subdomain modules or services. Very large projects may require further subdivision into Service-Oriented Architecture (SOA) or Cell-Based Architecture (a kind of Hierarchy).

Size-1

Any inherent decoupling within your domain is another factor to consider in the initial design. For example, the layer with domain logic is very likely to contain independent subdomains which naturally make modules or services at next to no development or runtime cost. Likewise, Top-Down Hierarchy is a good fit for a hierarchical domain. A domain that builds around stepwise processing of data or events may be modeled as a Pipeline, which is a very flexible architectural style.

Size-2

The number of teams you start the project with is also important. For the teams to be as efficient as possible you want them to be almost independent. As every team gets ownership of one or two components, you must assure that the architecture has enough modules or services for the teams to specialize, because anything shared will likely become a bottleneck. For example, you can hardly employ more than 3 teams with a layered architecture as there are only so many layers in any system. Thus, having a large number of teams strongly hints at Services, Pipeline, SOA, or Hierarchy.

If not all the teams are available from day one, it is still preferable to initially set up component boundaries for the prospective number of teams because subdividing an already implemented component is a terrible experience. However, it may be easier and safer for now to leave all the components running within a single process (as modules) to avoid the overhead of going distributed and have less trouble moving pieces of code around as needed (as new requirements often make a joke of your original design). You should be able to make modules into services through moderate effort once that becomes an imperative.

Domain features #

We’ve already seen above that hierarchical or pipelined domains enable the use of corresponding architectures. There is more to it.

Sometimes you expect to have many complex use cases which cannot be matched to your subdomains because every scenario involves multiple components, thus spreading over the entire system. You would usually collect the global use cases into a dedicated component – an Orchestrator. And if the Orchestrator grows out of control, it is subdivided into layers or services.

Features-1

Other systems are built around data. You cannot split it into private databases because almost every service needs access to the whole which necessitates a Shared Repository, or the highly performant Space-Based Architecture.

Features-2

Once you go distributed, you will likely employ a Middleware to centralize communication between your services. And you will have various Proxies, such as a Firewall, a Reverse Proxy, and a Response Cache. You may even deploy a Proxy per kind of client if the clients vary in protocols, resulting in Backends for Frontends (BFF).

Features-3

Runtime performance #

Moreover, there are non-functional requirements, such as performance and fault tolerance.

High throughput is achieved by sharding or replicating your business logic or even your data. Sharding also helps process huge datasets while replication improves fault tolerance. Space-Based Architecture replicates the entire dataset in memory for faster access.

Performance-1

Alternatively, you may use several specialized databases (Polyglot Persistence) or redesign a highly loaded part of your system as a self-scaling Pipeline.

Performance-2

Scalability under uneven load is achieved through Function as a Service (Nanoservices), Service-Mesh-based Microservices and, to a greater extent, Space-Based Architecture.

Performance-3

Fault tolerance requires you to have replicas of every component, including databases, ideally over multiple data centers. If you are not that rich, be content with Actors or Mesh.

Performance-4

Low latency makes you place simplified first response logic close to your input, leading to:

Performance-5

Flexibility #

If your product needs customization, you go for Plugins.

If it is to survive for a decade, you need Hexagonal Architecture to be able to swap vendors.

If you mediate between resource or service providers and consumers, you build a Microkernel.

Flexibility-1

When your teams develop services and you want them to be less interdependent, you insert an Anticorruption Layer, Open Host Service, or CQRS View between them.

Flexibility-2

When you have built a large system and really need that thorough data analytics, consider implementing a Data Mesh.

Data Mesh

Every domain is unique #

No one-size-fits-all. Embedded projects or single-player games don’t have databases and run in a single process. High Frequency Trading bypasses the OS kernel to save microseconds. Middleware and distributed databases care about quorum and leader election. Huge-scale data processing must account for bit flips. A medical device should never crash. Banks store their history forever for external audits.

There is no universal architecture. No silver bullet pattern. Patterns are mere tools. Know your tools and choose wisely.

So it goes #

Software architecture lies lifeless in my hands, devoid of its magical colors, like the dead iguana.

CC BY Denys Poltorak. Editor: Lars Noodén. Download from Leanpub or GitHub. Powered by odt2wiki and Hugo Book.