Layers: help large projects #
The main drawback (and benefit) of Layers is that much or all of the business logic is kept together in one or two components. That allows for easy debugging and fast development in the initial stages of the project but slows down and complicates work as the project grows in size. The only way for a growing project to survive and continue evolving at a reasonable speed is to divide its business logic into several smaller, thus less complex, components that match subdomains (bounded contexts [DDD]). There are several options for such a change whose applicability depends on the domain:
- The middle layer with the main business logic can be divided into Services leaving the upper Orchestrator and lower database layers intact for future evolutions.
- Sometimes the business logic can be represented as a set of directed graphs which is known as Event-Driven Architecture.
- If you are lucky, your domain is naturally a Top-Down Hierarchy.
Divide the domain layer into Services #
Patterns: Services, Shared Database (Shared Repository), Orchestrator.
Goal: make the code simpler and let several teams work on the project efficiently.
Prerequisite: the low-level business logic comprises loosely coupled subdomains.
It is very common for a system’s domain to consist of weakly interacting bounded contexts [DDD]. They are integrated through high-level use cases and/or relations in data. For such a system it is relatively easy to divide the domain logic into Services while leaving the integration and data layers shared.
Pros:
- You get multiple specialized development teams.
- The largest and most complex piece of code is split into several smaller components.
- There is more flexibility with deployment and scaling.
Cons:
- Future changes in the overall structure of the domain will be harder to implement.
- System-wide use cases become somewhat harder to debug as they span over many components.
- Performance may degrade if the Services and Orchestrator become distributed.
Further steps:
- Continue by splitting the Orchestrator and database, resulting in Orchestrated Three-Layered Services.
- Divide the Orchestrator (by type of client) into Backends for Frontends.
- Use multiple databases in Polyglot Persistence.
- Scale well with Space-Based Architecture.
Build an Event-Driven Architecture over a Shared Database #
Patterns: Event-Driven Architecture (Pipeline (Services)), Shared Database (Shared Repository).
Goal: untangle the code, support multiple teams, improve scalability.
Prerequisite: use cases are sequences of loosely coupled coarse-grained steps.
If your system has a well-defined workflow for processing every kind of input request, it can be divided into several subdomain services, each hosting a few related steps of multiple use cases. Each service subscribes to inputs from other services and/or system’s clients and publishes output events.
Pros:
- The code is divided into much smaller (and simpler) segments.
- It is easy to add new steps or use cases as the structure is quite flexible.
- You open a way to having several almost independent teams, one per service.
- You can achieve flexible deployment and scaling as the services are stateless, but you need a Middleware for that.
- The architecture naturally supports event replay as the means of reproducing bugs or testing / benchmarking individual components.
- There is no need for explicit scheduling or thread synchronization.
Cons:
- The system as a whole is hard to debug.
- You will have to live with high latency.
- You may end up with too many components which are interconnected in too many ways.
Further steps:
- Add a Middleware that supports scaling and failure recovery.
- Split the Shared Database by subdomain, yielding Choreographed Two-Layered Services.
- Scale with Space-Based Architecture.
- Extract the logic of use cases into an Orchestrator.
Build a Top-Down Hierarchy #
Patterns: Top-Down Hierarchy (Hierarchy).
Goal: untangle the code, support multiple teams, earn fine-grained scalability.
Prerequisite: the domain is hierarchical.
Splitting the lower layers into independent components with identical interfaces simplifies the managing code and allows the managed components to be deployed, developed, and run independently of each other. Ideally, the mid-layer components should participate in decision-making so that the uppermost component is kept relatively simple.
Pros:
- Hierarchy is easy to develop and support with multiple teams.
- Individual components are straightforward to modify or replace.
- The components scale, deploy, and run independently.
- The system is quite fault tolerant.
Cons:
- It takes time and skill to figure out good interfaces.
- There are many components to administer.
- Latency is suboptimal for system-wide use cases.