3 domain-driven steps you can take to get your codebase into a more manageable state
Let’s face it, writing code to be part of a monolith is easy. We can query the database directly whenever we want, call whatever functions we want in other parts of the application, and not have to think much about organization because we are plugging into an existing architecture. However, the problem that this type of development leads to is a fragile, entangled codebase in which any change to one part of the application can alter or even break something in some other part without anyone knowing why. Not only that, it also creates an awful learning curve for new developers coming into the codebase. This is not desirable at all and many who find themselves in this situation begin to read about and understand the upsides of a service-based architecture (SOA). The problem is — the larger the monolith, the harder it is to break up.
But just because something is hard, does not make it impossible. That being said, it can feel impossible because it is not very feasible to go from a messy monolith directly to an SOA. There has to be some kind of intermediary state we can get to that will make it easier to start breaking things up. This intermediary state is one that is still a monolith, but one that is organized by domain without the entanglement or fragility of our original codebase. Once we reach that point, it is much easier to make decisions about the future of our application, specifically in regards to deciding what to break out into services and what parts should remain together.
In this post, we will introduce domain-driven design (DDD) and and then go over three steps you can take to transform a messy monolith into the organized intermediary state just described.
Before beginning any refactoring, we have to figure out what the domains of our application are. The easiest way to do this is by breaking your application down into components that can be explained in terms of the business logic. For example, in a checkout application, some of the domains might be address validation, shipping, taxes, and payment processing. In software, this act of breaking down or building your code to be organized by business logic is called domain driven design (DDD).
One of the main concepts in DDD is organizing your code responsibility by subdomain within some overall bounded context. In the example of our checkout application, checking out is a bounded context within a larger eCommerce platform while shipping and taxes would each be subdomains in the checkout context.
The reason why domain-driven design can help us achieve an SOA is that through breaking up our code into individual domains, we are inherently creating service-like parts that come together to form our application. Then, if desirable, these service-like parts can then be broken out into their own microservices that the main application calls upon. Alternatively, if you decide to keep the monolith, you now have it in a state that is easy to iterate on and easy to understand what is going on in your application.
That being said, lets get into some of the steps we can take to organize our code.
Step 1— Quarantine your business logic based on domain to eliminate interdependency
The word “quarantine” was purposefully chosen here because it means more than just isolation. By quarantine, we don’t just mean separating functions out into different files, we want to take it a step further and not even let them touch other parts of the application unless we specifically want it to by injecting a dependency.
When building applications, it is often the case that your logic will touch across many different domains. Think again of a checkout process in an eCommerce application. A single checkout involves validating the shipping address, calculating the tax rates, fetching the shipping rates from a carrier, validating that the inventory is available, …the list goes on. It is entirely possibly to handle all of checkout in one process, but that would just make your code hard to maintain and nearly impossible to test. Instead, we want to completely separate all of the logic concerned with each of those parts such that they are not touching each other at all.
Keeping your application logic sorted by domain, is the first step towards a having a service-based architecture. If you have controllers or utility files with a grab bag of different functions, start by separating them out and organizing them by responsibility and removing the interdependencies of each function.
Step 2 — Define your interface and hide away everything else
One of the downsides to working in a monolithic codebase is that there is a huge learning curve for a new developer. It is overwhelming looking at a codebase for the first time and everything is in one place and you have no idea what calls what and where that happens. All of the logic is in one place and you have no idea where to start.
As developers, we shouldn’t have to understand the inner workings of every other part of the application in order to work in the codebase. One of the advantages that DDD brings us is that it allows us to think of our application in terms of what it is doing from a business logic point of view. All of the logic in each domain should be represented by a single interface that can be used to understand the capabilities of everything hidden behind it.
Here is an example of an interface that calculates the tax rate on an order.
Just by looking at this interface, we can infer a lot about how it works without even looking at the code. It uses a destination address, a shipping origin, and a store’s tax nexus to get a tax rate for an order. Even in a monolithic codebase, this practice of hiding all of the details behind a single interface is a good habit to get into.
Step 3— Use immutable Value Objects
This is where value objects come in. Value objects are not model instances and they do not have an ID. They are immutable containers of information with defined properties and their state is entirely dependent upon its values. Here is an example:
It may, at first, seem tedious and unnecessary to do this, but it gives us the confidence that in a system that operates on tax rates, we will always be working with tax rates and not random blobs of values that may or may not have the fields we need.
Going from a monolith code to a SOA is a daunting task, and it’s impossible to do it in one step. If you find yourself in a position in which your codebase has become too large to quickly iterate on, do not immediately start trying to hack it up. Instead, make it your goal to organize your monolith into well-defined subdomains using the DDD concepts described in this post. Once that is done, it will be much easier to start breaking out code as individual services.