Monolith: to Layers #
Another drawback of Monolith is its … er … monolithism. The entire application exposes a single set of qualities and all its parts (if they ever emerge) are deployed together. However, life awards flexibility: parts of a system may benefit from being written in varying languages and styles, deployed with different frequency and amount of testing, sometimes to specific hardware or end users’ devices. They may need to vary in security and scalability as well. Enter Layers – a subdivision by the level of abstractness:
- Most Monoliths can be divided into three or four layers.
- It is common to see the database separated from the main application.
- Proxies (e.g. Firewall, Cache, Reverse Proxy) are common additions to the system.
- An Orchestrator adds a layer of indirection to simplify the system’s API for its clients.
Divide into Layers #
Patterns: Layers.
Goal: let parts of the system vary in qualities, improve the structure of the code.
Prerequisite: there is a natural way to separate the high-level logic from the low level implementation details and dependencies.
Most systems apply layering by default as it grants a lot of flexibility at very little cost. Common sets of layers are: UI, tasks (orchestration), domain (detailed business rules) and infrastructure (database and libraries) or frontend, backend and data.
Pros:
- It is a natural way to specialize and decouple two or three development teams.
- The layers may vary in virtually any quality:
- They are deployed and scaled independently.
- They may run on different hardware, including client devices.
- They may vary in programming language, paradigm and release cycle.
- Most changes are isolated to a single layer.
- Layering opens a way to many evolutions of the system.
- The code becomes easier to read.
Cons:
- Dividing an existing application into Layers may take some effort.
- There is a small performance penalty.
Use a database #
Patterns: Layers, Shared Database (Shared Repository).
Goal: avoid implementing a datastore.
Prerequisite: the system needs to query (maybe also persist) a large amount of data.
A datastore is non-trivial to implement. While ordinary files are good for small volumes of data, as your needs grow so needs to grow your technology. Deploy a database.
Pros:
- A well-known database is sure to be more reliable than any in-house implementation.
- Many databases provide heavily optimized algorithms for querying data.
- You can choose other hardware to deploy the database to.
- Your (now stateless) application will be easy to scale.
Cons:
- Databases are complex and require fine-tuning.
- You cannot adapt the database engine to your evolving needs.
- Most databases do not scale.
- You are stepping right into vendor lock-in.
Further steps:
- Deploy multiple instances of your application behind a Load Balancer.
- Continue the transition to Layers by separating the high-level and low-level business logic.
- Polyglot Persistence improves performance of the data layer.
- CQRS passes read and write requests through dedicated services.
- Space-Based Architecture is low latency and allows for dynamic scalability of the whole system, including the data layer.
- Hexagonal Architecture will allow you to switch to another database in the future.
Add a Proxy #
Goal: avoid implementing generic functionality.
Prerequisite: Your system serves clients (as opposed to controlling hardware).
A Proxy is placed between your system and its clients to provide generic functionality that otherwise would have to be implemented by the system. The kinds of Proxy to use with Monolith are: Firewall, Cache, Reverse Proxy, and Adapter. Multiple Proxies can be deployed.
Pros:
- You save some time (and money) on development.
- A well-known Proxy is likely to be more secure and reliable than an in-house implementation.
- You can select hardware to deploy the Proxy to.
Cons:
- Latency degrades, except for Response Cache where it depends on frequency of requests.
- The Proxy may fail, which increases the chance of failure of your system.
- Beware of vendor lock-in.
Further steps:
- Another kind of Proxy may be added.
- Some systems employ a Proxy per client, leading to Backends for Frontends.
Add an Orchestrator #
Patterns: Layers, Orchestrator.
Goal: provide a high-level API for clients to improve their developer experience and performance.
Prerequisite: the API of your system is fine-grained and there are common use cases which repeat certain sequences of calls to your API.
A well-designed Orchestrator should provide a high-level API which is intuitive, easy to use, and coarse-grained to minimize the number of interactions between the system and its clients. An old way to access the original system’s API may still be maintained for rare use cases or legacy client applications (open orchestration). As a matter of fact, you program common logic on behalf of your clients.
Pros:
- Client applications become easier to write.
- Latency improves.
Cons:
- You get yet another moving part to design, test, deploy, and observe; and lots of meetings between development teams for a bonus.
- The new coarse-grained interface will likely be less powerful than the original one.
Further steps:
- Backends for Frontends use an Orchestrator per client type.
Further steps #
Applying one of the evolutions discussed above does not prevent you from following another one of them, or even the same one for a second time:
- A layer can be split into two layers.
- A database can be added.
- Multiple kinds of Proxies are OK.
- If you don’t have an orchestration layer yet, you may add one.
Those were evolutions from Layers to Layers.
Another set of evolutions stems from splitting one or more layers into Services:
- Splitting a Proxy and/or Orchestrator yields Backends for Frontends where requests from each kind of client are processed by a dedicated component.
- Splitting the layer with the main business logic results in Services, possibly augmented with layers of Middleware, Shared Database, Proxies and/or Orchestrator.
- Splitting the database layer leads to Polyglot Persistence with specialized storages.
- If all the layers share the domain dimension and are split along it, Layered Services (or its subtype CQRS) emerge.
- If each layer is split along its own domain, the system follows Service-Oriented Architecture that is built around component reuse.
- Finally, some domains support Hierarchy – a tree-like architecture where each layer takes a share of the system’s functionality.
In addition,
- Distributed systems usually allow for the scaling of one or more layers.
- A layer may employ Plugins for better customizability.
- The UI and infrastructure layers may be split and abstracted according to the rules of Hexagonal Architecture (or its subtype Separated Presentation).
- The system can often be extended with Scripts, resulting in a kind of Microkernel.