Monolith: to Services #
The final major drawback of Monolith is the cohesiveness of its code. The rapid start of development begets a major obstacle for project growth: every developer needs to know the entire codebase to be productive and changes made by individual developers overlap and may break each other. Such distress is usually solved by dividing the project into components along subdomain boundaries (which tend to match bounded contexts [DDD]). However, that requires a lot of work, and good boundaries and APIs are hard to design. Thus many organizations prefer a slower iterative transition.
- A Monolith can be split into Services right away.
- Or only the new features may be added as new services.
- Or the weakly coupled parts of existing functionality may be separated, one at a time.
- Some domains allow for sequential data processing best described by Pipelines.
Divide into Services #
Patterns: Services.
Goal: facilitate development by multiple teams, improve the code, decouple qualities of subdomains.
Prerequisite: there is a natural way to split the business logic into loosely coupled subdomains, and the subdomain boundaries are sure to never change in the future.
Splitting a Monolith into Services by subdomain is risky in the early stages of a project while the domain understanding is evolving (in-process modules are less risky but provide fewer benefits). However, this is the way to go as soon as the codebase becomes unwieldy due to its size.
Pros:
- Supports multiple, relatively independent and specialized development teams.
- Lowers the penalty imposed by the project’s size and complexity on the velocity of development and product quality.
- Each team may choose the best fitting technologies for its service.
- The services can differ in non-functional requirements.
- Flexible deployment and scaling.
- A certain degree of error tolerance for asynchronous systems.
Cons:
- It takes lots of work to split a Monolith.
- Any future changes to the overall structure of the domain will be hard to implement.
- Sharing data between services is complicated and error-prone.
- System-wide use cases are hard to understand and debug.
- There is a moderate performance penalty for system-wide use cases.
Add or split a service #
Patterns: Services.
Goal: stop digging, get some work for novices who don’t know the entire project.
Prerequisite: the new functionality you are adding or the part you are splitting is weakly coupled to the bulk of the existing Monolith.
If your Monolith is already hard to manage, but a new functionality is needed, you can try dedicating a separate service to the new feature(s). This way the Monolith does not become larger – it is even possible that you will move a part of its code to the newly established service.
If you are not adding a new feature but need to change an old one – use the chance to make the existing Monolith smaller by first separating the functionality which you are going to change from its bulk. At the very minimum this two-step process lowers the probability of breaking something unrelated to the changes of behavior required.
Pros:
- The legacy code does not increase in size and complexity.
- The new service is transferred to a dedicated team which does not need to know the legacy system.
- The new service can be experimented with and even rewritten from scratch.
- The likely faults of the new service don’t crash the main application.
- The new service can be tested and deployed in isolation.
- The new service can be scaled independently.
Cons:
- The new service will have a hard time sharing data or code with the main application.
- Use cases that involve both the new service and the old application are hard to debug.
- There is a moderate performance penalty for using the service.
Further steps:
- Continue disassembling the Monolith.
Divide into a Pipeline #
Patterns: Pipeline (Services).
Goal: decrease the complexity of the code, make it easy to experiment with the steps of data processing, distribute the task over multiple CPU cores, processors or computers.
Prerequisite: the domain can be represented as a sequence of coarse-grained data processing steps.
If you can treat your application as a chain of independent steps that transform the input data, you can rely on the OS to schedule them and you can also dedicate a development team to each of the steps. This is the default solution for a system that processes a stream of a single type of data (video, audio, measurements). It has excellent flexibility.
Pros:
- Nearly abolishes the influence of project size on development velocity.
- The project’s teams become almost independent.
- Flexible deployment and scaling.
- Naturally supports event replay for reproducing bugs, testing or benchmarking individual components.
- It is possible to have multiple implementations of each of the steps of data processing.
- Does not need any manual scheduling or thread synchronization.
Cons:
- Latency may skyrocket.
- As the number of supported scenarios grows, so does the number of components and pipelines. Soon there’ll be nobody who understands the system as a whole.
Further steps #
As your knowledge of the domain and your business requirements change, you may need to move some functionality between the services to keep them loosely coupled. Sometimes you have to merge two or three services together. So it goes.
Systems of Services or Pipelines are quite often extended with special kinds of layers:
- Middleware takes care of deployment, intercommunication and scaling of services.
- Shared Repository lets services operate on and communicate through shared data.
- Proxies are ready-to-use components that add generic functionality to the system.
- Orchestrator encapsulates use cases that involve multiple services, so that the services don’t need to know about each other.
- Finally, there are Combined Components that implement two or more of the above patterns in a single framework.
Each service, being a smaller Monolith, may evolve on its own. Most of the evolutions of Monolith are applicable. The most common examples include:
- Scaled (Sharded) Service with a Load Balancer and Shared Database to support high load.
- Layered Service to improve the code structure and decouple deployment of parts of a service.
- Cell (Service of Services) to involve multiple teams and technologies within a single subdomain.
- Hexagonal Service to escape vendor lock-in.