How to Architect a Well-Modularized System with Domain-Driven Design

Time to read
10 min
Category
How to Architect a Well-Modularized System with Domain-Driven Design
No Results Found
Table of Contents
  • What is a big ball of mud in domain-driven design?
  • The benefits and risks with choosing microservices over a monolithic architecture
  • How to split projects into smaller pieces with DDD as a pattern
    • Login page
    • Payout options
    • Verification
  • Strategic domain-driven design
    • Bounded contexts
    • Integrating contexts
    • Context maps
    • Dependency types
    • Finding bounded contexts
  • Modular monolith
  • Microservices
  • Microservices vs. modular monolith comparison
  • Final thoughts on building the architecture for a well-modularized system with DDD

Domain-driven design (DDD) is a software development approach that helps you solve complex problems by connecting the implementation to an evolving model.

DDD comprises an array of tools that essentially capture the domain model in domain terms, embed the model in the code, and protect it from corruption. It can also be thought of as a process where software development is driven by design and user experience.

However, all of that brings us to the question, “How do you implement DDD in your system?” You’ll find out if you keep reading, along with other related concepts such as:

  • Defining a big ball of mud in DDD
  • Comparing microservices and monoliths
  • Splitting projects into smaller bits with domain-driven design
  • Incorporating DDD into your system strategically

The following guide is based on a presentation given by Sebastian Buczyński, Technical Lead at Sauce Labs, during our PowerIT Conference to celebrate 30 years of Python. Head over here to watch the full recording of Sebastian’s webinar on the subject:

 

What is a big ball of mud in domain-driven design?

Even if its creation began with a clean slate, every product eventually turns into a big ball of mud.

The most common way to design and architect a software solution—a big ball of mud—refers to a code jungle that comprises unsystematically structured, messy, sprawling code bound together essentially by duct tape.

Though this definition might sound odd and appear to be going against the software design principles you follow, chances are that you have or will work on a project with these characteristics. Because the science of programming and software development, including those involving Python, contributes to creating a new big ball of mud every day.

There’s always a good reason why projects turn into a big ball of mud. Perhaps it’s the high turnover rate, mounting pressure, or lack of knowledge, experience, or understanding of a project.

While there have been countless attempts to solve this issue, most fail to offer the anticipated result. Of the few that worked, a notable solution involved microservices.

The benefits and risks with choosing microservices over a monolithic architecture

One of the main selling points of microservices includes splitting a monolith into several smaller parts. However, microservices are a lot more complex than monolithic systems. So, if organizing your code is your primary goal, you might want to reconsider your choice of switching to microservices.

Most people who work with a microservice architecture end up dividing it the wrong way. The system is split just for the sake of being micro, and in the end, managing and maintaining dozens of microservices becomes a nightmare when there are only a few developers in your team.

However, the bottom line with microservices is that you get rid of one big problem and instead deal with a few smaller ones. While the concept isn’t necessarily flawed, perhaps you don’t have to distribute your application always. Nevertheless, if the need for microservices still arises, you need a robust way to divide your system into smaller, manageable parts.

This is exactly what we will discuss in the upcoming section: how to split a project into small pieces and keep order with domain-driven design as a pattern.

How to split projects into smaller pieces with DDD as a pattern

Since we began this article by mentioning clean slates, let’s start a new sample project—a bookmaking platform.

Login page

extrabookmaker login page screenshot

Like on every platform, we begin with a login page, where you enter your email and password.

If you’ve worked on similar projects previously, you might already know which framework to use or whether you should leverage code or a cloud service.

Nonetheless, the core of a bookmarking platform is the ability to make bets. The system should be able to tell you the odds for a given game and your potential investment to bag profits.

Payout options

Speaking of profits, you need to offer users a way to withdraw their earnings.

Meaning, a list of bank accounts or integration of banks.

Verification

extrabookmaker verification screenshot

Integrating payment options, however, isn’t that simple. First, you must verify users who register an account to stay compliant with the laws in your region. For instance, in Poland, the bookmaking service provider should ensure a user is 18 years of age or older and undergo the KYC procedures before they can legally wager on the platform.

So, you also need to incorporate a verification page into your bookmaker platform with fields like a picture, DOB, ID, etc. However, things don’t end there. Now, you also have to verify the applications you receive through automation.

After creating a good number of elements for our bookmaker platform, if you look at the image below, you will see just how much of a scary big ball of mud it has turned into.

extrabookmaker platform screenshot

Well, the problem here is that we’re imagining real scenarios and turning them into code. Either that, or we’re coming up with a mental model of how things should work and the system should be and quickly translate them into code. This is what resulted in the creation of a big ball of mud.

Strategic domain-driven design

First things first, you shouldn’t be creating a single model, as it can lead to disaster. Additionally, any model you create will only make sense within a specific context.

For example, if we look at the user interface we created earlier, we could think of the user as a person who clicked through all the screens. Also, we might have been wondering about the problem specific to the UI—how to ensure they click that button, how to ensure they spend their money, and how to ensure there is no friction.

An old motto becomes relevant here: divide and conquer. A school of thought derived from domain-driven design says that to create a maintainable system, you must have multiple contexts and models.

Bounded contexts

Bounded contexts photo

Contexts can be bounded. For instance, the customer model we created for the bookmaker platform doesn’t make sense outside the context, because there is no notion of a user or whoever gets verified.

To make the concept of bounded contexts more approachable, we have added different perspectives to the user. So, like with authentication, there is a subject—a party that authenticates against gambling, a gambler, and a customer/KYC.

So, in this case, differentiating various bounded cases becomes easy in our software. This is a crucial phase, as it’s the first strategic pattern of our domain-driven design.

Though these various models make analyzing reality easy, they should still communicate and work together as a single system. However, how can we settle the differences when all the models look at reality differently?

The simple answer is to help them find common ground.

Integrating contexts

integrating contexts photo

Each model should communicate with the other. For example, in general, the gambling context that deals with gambling asks the KYC context if a user is verified and eligible to gamble.

However, since both models speak a different language, we choose what language we use to communicate. So, for instance, we can sacrifice a bit of KYC and focus on gambling by simply answering the question, can a user gamble?

This process of bounded contexts communicating with each other and deciding whose language to use is called a context map.

Context maps

Another essential component of strategic domain-driven design—the context map—revolves around the process of deciding on the language to be used in communications or defining whose model is superior.

At the very beginning, you have little to no idea about models, their languages, and their superiority. So, you will have to perform some analysis in advance or work on the project for a long time before you can develop a context map.

Dependency types

Dependecies types photo

We can take this further by making a bounded context of subscriptions to tell the KYC and gambling model the priority and set the fee. However, the entire functionality here is not just about subscription but also about introducing a priority concept to the KYC bounded context.

As such, the process has now become much simpler. Another point to note here is that the language and run time dependency can always be flipped around, however you like or want.

Subscription is a component that can affect your system’s functionalities. So, it is important to ensure that the entire project doesn’t depend on it.

Finding bounded contexts

To find bounded contexts, you will essentially have to look at the following heuristics:

  • What is the subdomain and problem space?

You should first analyze the business, the software application you will create for the business, the different areas of this company’s activity, how they make money, their additional operations, law obligations, and others.

  • Are we considering different stakeholders?

Chances are that some parts of the code are being created for different stakeholders. For example, KYC is the only driver of data on our platform. So, instead of sending cool emails, it should focus on making users aware of the law obligations, which is entirely different from the stakeholder that helps the company bag more profits.

  • Can we sell it separately? Has anyone already done that?

This is also a case of KYC. There are several companies that might want to put people in the databases of known vendors.

  • Is there a single source of truth for every fact?

If there is no single source of truth for every fact, it becomes impossible to have the same information everywhere. For example, subscription is the source of truth for priority KYC handling or its fee. However, since it is a source of truth, KYC, gambling, or any other context can’t keep a cache of it. So, if you wish to learn about a single fact, you will have to go to a single-bounded context.

  • Can we observe phenomena of data or feature entry?

When a bounded context needs to perform some operation but there’s a lot of communication happening with other bounded contexts, these context boundaries might go wrong. Therefore, it requires more insights.

  • Do we use the same names for things with different behavior?

This is another heuristic that points out you might have the same names for different things that behave differently in your system. This phenomenon can be particularly observed at the level of individual classes. For example, we previously saw a case where we used a very generic username for a few different concepts.

  • Are we confusing the UI with the business model?

We may be confusing the user interface way of looking at things with how the business works. This was the case with the bookmaking platform example we presented earlier in the article. 

So, now we have a beautiful context map in hand. But how do we implement it without creating a code jungle, a.k.a. a big ball of mud?

There are two ways to go about it: a modular model or a microservices system.

Modular monolith

modular monolith photo

With modular monoliths, we cut along the line of the bounded context and create different components out of it. Alternatively, we combine the authentication context with a web application, so there won’t be a need for a separate context or code, as there are already libraries to do that.

An important thing to keep in mind is that they need to have separate namespaces. So, in the code, you can have a single root package and several subpackages.

components-as-separated-namespaces-photo

Every component should have an API. In the gaps of the component, you can use its API to address the services it offers. For example, in the case of KYC, it can be a facet design pattern to expose the corresponding functionalities. This can assign “gamble” to “gambling” and set the priority for subscriptions.

All components are encapsulated just like the objects are. So, they contain some code meant to be private, not to be used from the outside, and also code meant to be used outside, like the facets. This means the public elements are API, and the private elements are all mechanics and internal stuff.

Now, Python might not support that directly, but there are tools that do. For instance, if all properties are declared on a module level, PyCharm would notify you when you shouldn’t use it because it isn’t declared down the road. Though the program will work, the tools discourage you from doing so.

components are encapsulated photo

import linter photo

Microservices

In the case of microservices, we start with cutting along the line, then we move on to using several patterns of microservices like back-and-forth frontend. That means there seems to be a service that acts as an umbrella for multiple microservices and can also provide a means of authentication.

microservices patterns photo

Here, we also consider some architectural drivers, such as the things required to scale or deploy independently. Again, this is a crucial point to consider when translating our context map into code.

Microservices vs. modular monolith comparison

To sum up, the API modular monolith is ideal for directly calling one context to another. With microservices, we have to call an API of another microservice or send a message through a broker.

microservices vs modular monolith photo

When it comes to the inversion of the direction, we can use the abstract class or concrete class in the modular monolith. On the other hand, with microservices, we get a dedicated API that might be created through a technique known as consumer-driven contract testing.

Lastly, with events, it gets simpler with the modular model, since you can use libraries like Blinker or Django Signals or send a message to the broker, whereas with microservices, you always have to send a message to the broker.

Final thoughts on building the architecture for a well-modularized system with DDD

Better code isn’t just rooted in better programming skills. It also takes a great deal of understanding of the problem you’re dealing with.

So, even though DDD might not be a one-size-fits-all solution to every problem ever in the world of programming, it is a complete and comprehensive technique to drive better software design. In the end, it will provide you with a much better project than a big ball of mud.

Thank you for reading our article! We hope you found it useful. If you wish to get more expert advice on software development, consider checking out the following resources on our website:

Last but not least, if you need any support with your future projects, we have literally hundreds of professionals at the ready who can help you out. Feel free to take a look at our services and reach out to us for assistance!

Get your free ebook

Download ebook
Download ebook
Content Writer
Principal Software Enginner
Share this post