Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design
8 min read

Facing our subject - tightly-coupled monolith code

As a software architect, you might find yourself facing the daunting task of decoupling coupled monolithic applications. Monolithic applications, while being efficient and reliable in certain cases, often suffer from tight coupling and inflexibility which can lead to reduced development speed, scalability issues, and complexity in managing the application's lifecycle. One approach to overcome these challenges is by adopting event-driven and domain-driven design strategies to enhance modularity, scalability, and manageability.

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

Just a few lines of code, but already with a dangerous coupling of four different system parts - Product, Order, Notification System and Shipment. When does the application fires different emails depending on Order updates? What if several places of the application triggers same or similar updates and you need to change the behavior? How can you understand the whole logical automata of the system - what can happen inside and how will different parts of it react? The larger this application will grow, the harder would be to reason about it, to build and keep in focus a mental model of it, and to introduce changes to the business logic.

A monolithic App is often the right choice to start with

Starting with a monolithic application architecture is not necessarily a mistake and is often the right choice, especially for small to medium-sized projects. Monoliths have their advantages - they are easier to develop, test, and debug since all the codebase is in one place, they have atomicity in transactions, and the deployment process is relatively simple.

When the application's scope is not very large, and the team is small, a monolithic design can be more efficient and productive. It allows the team to focus on building and validating business features without getting tangled in the complexities of distributed systems.

It's only when the application starts to grow, both in terms of codebase size and team size, that the drawbacks of monolithic architecture such as inflexibility, scalability issues, and longer development cycles start to appear. This is when it's beneficial to consider moving to a more decoupled or microservices architecture. Premature optimization can be as harmful as no optimization. Building a monolithic application as a starting point can often be the most practical path.

The real issue is your logical backbone - distance x coupling of your system parts

The crux of the matter is not whether an application is monolithic or made up of microservices. The real issue lies in the inappropriate bindings and excessive coupling between different parts of the application. It is this unwarranted entanglement that gives rise to a host of problems like inflexibility, scalability issues, and complexities in code maintenance. The right balance of cohesion and decoupling, irrespective of the architectural style, is the cornerstone of a robust, maintainable, and scalable application.

Here’s a great talk about “distance x coupling” issue in the context of systems growth - https://www.youtube.com/watch?v=1ZyR_tgGTp8

Breaking Monoliths into Atomic Domains

The initial step in transitioning away from a monolithic architecture is to identify distinct domains within your application. This is where Domain-Driven Design (DDD) can play a pivotal role. DDD is an approach to software development that centres the software around the core business concepts (or domains) and their logic.

Start by breaking down your business requirements into logical domains, often called Bounded Contexts in DDD. A Bounded Context is a logical boundary within which a particular model is defined and applicable. Each of these contexts encapsulates specific business functionality and can be developed, tested, and deployed independently.

The benefit of segregating a monolithic application into multiple domains is the potential to isolate failures, simplify the codebase, and distribute the development workload across various teams.

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

"The Clean Architecture" or “Layered Architecture”. From the Uncle Bob archives.

Implementing Event-Driven Communication

Once you have identified your domains, the next challenge is to enable communication between them without introducing tight coupling. Here, an event-driven architecture can be highly beneficial.

In an event-driven system, when a domain performs an operation (for example, a 'user created' event in a User domain), it produces an event that other domains can subscribe to. These subscribing domains can then perform their operations based on the event data, independent of the producing domain. This mechanism promotes decoupling and ensures that each domain can evolve without impacting others.

Implementing an event-driven architecture requires an event bus or a message queue system like RabbitMQ, Apache Kafka, or AWS SQS, which can reliably transmit events from producers to consumers.

One Domain notifies others (picture from https://khalilstemmler.com/articles/domain-driven-design-intro/)

One Domain notifies others (picture from https://khalilstemmler.com/articles/domain-driven-design-intro/)

System parts and events (picture from https://khalilstemmler.com/articles/domain-driven-design-intro/)

System parts and events (picture from https://khalilstemmler.com/articles/domain-driven-design-intro/)

Transitioning to Microservices

With your application now divided into atomic domains and leveraging event-driven communication, you can consider the further step of breaking these domains down into microservices.

Microservices are small, independently deployable units that encapsulate a single piece of business functionality. They communicate with each other using well-defined APIs and protocols, usually over HTTP/HTTPS.

While it's tempting to jump straight into microservices, it's often advisable to first gain experience managing domains in a decoupled environment before going this route. When you're ready, though, transitioning to microservices can bring additional benefits, such as more fine-grained scaling, independent deployments, and the potential to use different technologies for different services.

Cross-Service Communication in Microservices

When moving to microservices, the question of inter-service communication becomes more crucial. Here, strategies like API Gateway and Service Mesh can be used.

An API Gateway acts as a single entry point for all client requests and routes them to the appropriate microservice. This allows the client-side to remain unaware of the individual microservices, enhancing the decoupling process.

A Service Mesh, on the other hand, manages the communication between microservices. Tools like Istio or Linkerd provide features such as load balancing, service discovery, traffic management, and fault injection, making managing microservices easier and more robust.

Example: Impact of decoupled code on testing

Let's illustrate the testing benefits of decoupled microservices over a tightly-coupled monolith using a user registration process as an example. The process might involve a User microservice for handling user data and an Email microservice for sending confirmation emails.

For the monolith application, the registration process might look something like this:

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

Testing this function could be cumbersome as it involves both user registration and email sending logic:

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

This test is quite involved as it has to deal with both database and email service. It would be complex to write, and changes in either of the registration or email sending logic might break it.

Now, let's look at how you might implement the registration process using microservices. You might have separate functions in each microservice:

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

You can test each function separately, focusing on their individual responsibilities:

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

In this example, each test is simpler and more focused because it only needs to deal with one concern. This makes the tests easier to write, understand, and maintain. It also makes it less likely that changes in one part of the system will break tests in other parts of the system.

Example: decoupling with events

Let’s take a look at an example of e-commerce application with Products and Orders.

Here's the tightly-coupled spaghetti code:

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

Now, let's decouple this logic using events:

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

Decoupling Monolithic Applications: Embracing Event-Driven and Domain-Driven Design

Conclusion

In essence, decomposing monolithic applications into atomic domains using DDD, adopting event-driven architecture for inter-domain communication, and eventually migrating to a microservices architecture can help overcome the challenges of managing large and coupled monolithic applications. It's a journey with its own set of challenges, but the rewards in terms of scalability, flexibility, and manageability can be substantial.

 

In case you have found a mistake in the text, please send a message to the author by selecting the mistake and pressing Ctrl-Enter.
Alex Shevtsov 2
Software engineer, team leader, and CTO with a knack for creating scalable software solutions and an eye for design
Comments (0)

    No comments yet

You must be logged in to comment.

Sign In / Sign Up