Decoding DDD: A Three-Tiered Approach to Django Projects with Pavel Sviridov

This video features Pavel Sviridov at DjangoCon US 2023 in Durham, North Carolina, USA.

Decoding DDD: A Three-Tiered Approach to Django Projects with Pavel Sviridov
0:24:46
Published November 22, 2023
2,167 views

Are you grappling with a project morphing into a 'Big Ball of Mud'? Are you conversant with Domain-Driven Design (DDD) but unsure about its application to Django projects? This talk will navigate you through three levels of implementing DDD in Django projects right from the ground up. Expect to walk away with the knowledge of transforming your Django project by employing DDD techniques at various levels based on your project's needs and constraints.

This talk was presented at: https://2023.djangocon.us/talks/decoding-ddd-a-three-tiered-approach-to-django-projects/

LINKS:
Follow Pavel Sviridov 👇
On Twitter: https://twitter.com/sviridov_pavel
Website: https://svipy.com/

Follow DjangCon US 👇
https://fosstodon.org/@djangocon
https://twitter.com/djangocon

Follow DEFNA 👇
https://www.defna.org/

Video production by the presenter and DjangoCon US 2023 volunteers.

Summary

Pavel Sviridov argues that Django and domain-driven design (DDD) work well together, especially for large projects that have become tightly coupled monoliths. He presents a three-level approach: first use ubiquitous language and bounded contexts to create a modular monolith; then enrich Django models with value objects, aggregates, and domain services; and, only where business logic is highly complex, separate plain-Python domain models from Django’s persistence layer using repositories and ports and adapters. The approach should be chosen pragmatically: the deeper levels add testing and separation benefits but also bring boilerplate, translation overhead, performance costs, and less convenient ORM queries.

Key takeaways

  • Ubiquitous language aligns developers and domain experts, while bounded contexts prevent different meanings of the same business concept from being forced into one model.
  • Bounded-context modules should own their data, hide implementation details, and communicate through data transfer objects or primitive types.
  • Django models can serve as rich domain models when their methods enforce business rules, with value objects representing concepts such as money or date ranges.
  • Aggregates protect invariants and provide transactional boundaries; only the aggregate root should expose operations that change state.
  • For highly complex domains, plain-Python domain models, repository interfaces, and Django-specific adapters can isolate business logic and make it easier to test.
  • DDD does not require every project or module to use its most elaborate patterns; different bounded contexts can use different levels of separation.

Summarised automatically from the transcript.

Chapters

  1. 0:00 Introduction to DDD in Django Pavel Sviridov introduces the talk and explains why domain-driven design can improve large Django projects.
  2. 4:17 DDD Levels and Strategic Design The talk establishes a three-level approach, beginning with strategic patterns for managing complexity.
  3. 5:03 Ubiquitous Language The speaker explains how shared terminology connects developers, business experts, and domain models.
  4. 7:27 Bounded Contexts and Modular Monoliths Different domain perspectives are separated into bounded contexts that become high-level modules in a modular monolith.
  5. 8:59 Context Boundaries and Event Storming The talk covers module organization, event storming, and practical rules for keeping bounded contexts independent.
  6. 10:33 Active Record and Rich Domain Models The speaker contrasts Django’s Active Record approach with richer domain models that encapsulate business behavior.
  7. 12:59 Value Objects Value objects such as money are introduced as explicit, immutable representations of domain concepts.
  8. 15:24 Entities and Aggregates The talk explains entity identity, aggregate roots, invariants, and transactional boundaries.
  9. 18:26 Domain Services and Level One Stateless domain services complete the first tactical level, combining bounded contexts, aggregates, and value objects.
  10. 19:21 Plain Python Domain Models The second level separates complex domain models from Django’s database representation using plain Python objects.
  11. 20:11 Repositories and Ports and Adapters Repository interfaces, Django implementations, and dependency inversion isolate domain logic from persistence.
  12. 22:32 Three-Layer Architecture and Tradeoffs Presentation, domain, and data-access layers are assembled, along with the costs and appropriate use cases of the deepest approach.

Transcript

2,869 words · auto-generated Show

Automatically transcribed, so expect mistakes in names and technical terms.

0:21

Hello everyone, my today's topic is the usage of domain-driven design in Django projects My name is Pavel Sviridov. I'm the head of software development at triple10. com and thanks for having me here. Over the last year, my focus has been on shaping the software architecture at our company. Couple of words about Triple 10. Uh we offers online bootcamps designed for beginners because we believe anybody, no matter their background, can learn how to code. Our lessons are fully part-time and fully remote and students grade 8 and 10 months. Students from all over the world can learn how to become

1:10

Coders, data scientists, software engineers, and more. And the heart of our backend is the Jenga project. There is a belief that Jenga and domain driven design are incompatible. If you read the comments on any article about DDD in Django, you will find many opinions suggesting that these patterns are suitable for Java, for C sharp, but not for Python and specially not for Jango. I aim to show that Django and DDD can complement each other effectively. Actually knowing strategic and tactical patterns can improve large Django projects making them easier to develop and maintain.

1:58

For developers unfamiliar with DDD, this talk will provide a quick overview of the patterns. For those who already uh have large Django project, this can offer insights on how to begin a refactoring. For developers, starting a new project, it can guide a better structure of the project. I will discuss in various DDD patterns, but we have a small time frame, so I can't cover everything. For a full understanding, I suggest reading a domain-driven design book. Django is a great framework and it gives you an opportunity for a quick start.

2:44

And the framework provides you some architecture that's good enough when your project is not very big. But usually it's hard to realize that moment when you need to stop your quick start trash and think about architecture of the project. You found that your project is huge, highly coupled, and not easy to maintain. And we fell to the trap too, and we had 85 applications, more than 2200 models. and more than 500 relations. And very slow tests. Some Teams starts to split such jungle monoliths into microservices But we are pragmatic and we understand that it's uh

3:30

if you can't build good monolith microservices would not help you. And it's really easy to get a distributed big ball of mud instead of monolithic big ball of mud. Creating microservices for reducing complexity is overhead. Therefore, we decided to transform our big ball of matte monolith to modular monoliths, just to split the project into modules following by separation of concern principle. But we did not know how to start. DDD proposed some ideas that sounds interesting for us DDD promotes conversation with business experts. This alliance with Scrum, which we also use.

4:17

We had already chosen to move away from software analysts. Because creating detailed requirements is challenging, if not impossible. This often results to late consultations with experts. And I would like to mention that tackling complexity is in the name of the original Eric Evans Blue Book and it is what we really needed. I split it my topic into three different parts. Each part is a level. It means that you can choose how far you want to go on using DDD in your project. But it also means that you can't jump over the levels. Level 0 is all about strategic

5:03

patterns, the most important ones from my point of view. Let's start from ubiquitous language. Ubiquitous language is a term that used in DDD for the practice of building up common language between business experts, developers, and users. From first view, ubiquitous language is not connected to implementation to Jenga. but it's crucial for understanding what bounded context is. And of course you will get a profit from building and using it without connecting to implementation. People think using mental models and language help us to build such models.

5:51

Because developers' mental models become software, it's crucial to speak the same language with business people and to build such ubiquitous language in collaborate with domain experts. Of course, such language must not contain technical jargon. There are some general tips how to build and how to maintain the language. Create a glossary, avoid using synonyms, if you find them being used, pose and agree on single term. Use it whatever in possible, in Jiro ticket, in documentation, in communication and in code. But

6:36

sometimes different experts may use the same term in different ways. It doesn't mean that they talk about different entities It means that each of experts have their own mental model of the entity. It does not mean that the model of one expert is right and the model of other expert is wrong All models are on, but some of them are useful, but useful only in context of some problem. And different experts solve different problems Here on the slide is a lesson in our learning management system. When we talked to content producers. They think that lesson is something that can be created, edited, and published for students.

7:27

But for students, it's something that they can learn, mark as past, and reset. It's all about the same entity. And the traditional solution for this issue is to design a single model that can be used for all kinds of problems. But it's something that had already led us to highly coupled big ball of mud with hundreds of relations. The solution in domain-driven design is trivial. Divide the ubiquitous language into multiple smaller languages Then assign each one to the explicit context in which it can be applied. It's a bounded context

8:12

And bounding context is something that we can use in the implementation. Context boundaries must align with the boundaries of microservices, or, in our case , with the boundaries of high-level module in our modular monolith. We decided to employ Bounded context as the highest level modules of our modular monolith. In theory, there is no one-to-one relationship between bounded context and modules The most important point is that the boundaries of module and boundaries of bounded context should not overlap. But we decided to simplify it.

8:59

Each module can either be a single Jenga app if it's small and straightforward or a set of Jenga apps. On the slide, there are five high-level folders. Three of them are just bounded context modules, share it kernel, which is the code that can be used in all of the context and project folders that contains Django specific project settings. But how to find the boundaries in your domain? Here is an event storming could help us. It's a collaborative workshop to explore the project's domain and identify the key concepts, events, and

9:45

process involved. If you don't use such a great tool in your project, try to. It is well described and formalized. Note that the choice of model boundaries is a strategic design decision Bounded contexts are not fixed. You can refine your architecture over time by creating additional context or merging existing ones. Here is a three points that describe how to deal with bounded context modules. Each module must have its own set of tables and ideally its own independent database. If transactional integrity between modules is necessary, utilize specific patterns such as two-phase

10:33

commit or saga. Modules should remain unaware of each other's internal details or behavior. So use data transfer objects and primitive types for communication between them If you already split all your models into bounded context. and started using ubiquitous language, then you already benefiting from DDD and you could stop here Or we can advance to another level and start using tactical DDD patterns to improve the code within bounded context modules. As you know, Django used ActiveRecord Pattern.

11:21

It encapsulates the complexity of mapping the in-memory object to the database schema. It's good enough for crowd operations and simple business logic, but often using active record leads to anemic domain model anti-pattern. Anonymic domain model refers to a design where the domain object lacks significant behavior and primary contain data. with the actual domain logic situated in separate external code. In this example, admission is an animic model. Because the external code, the view in this case, modifies the state of the model and uses this model as a data storage.

12:08

Admission does not encapsulate its state This is a common situation in Django because the framework does not provide mechanisms to hide intro-intern state of the model. In core of ZDD strategic patterns is a domain model pattern. It's sometimes called rich domain models in constant to anemic ones The domain model patterns is designed to handle complex business logic. Here, instead of CRAD interfaces, we address intricate stained transitions and business rules All functions that modify the state of such a model should use terminology consistent with the bounded context's ubiquitous language.

12:59

In other words, these patterns allows the code to speak the ubiquitous language and align with the domain expert's mental model. At this level, we use Django models as domain models, and our goal is to make them as rich as possible. Let's explore patterns that allow us to do it Value object. A value object is an object that can be identified by the composition of its values Good examples are daytime range, money, email, anything that can compare using its values Using only basic language types like string, integers, or dictionaries for business concepts known as primitive obsession

13:51

codes mell. Django score provide only primitive model fields and when you interact with Jenga model you usually use primitive types. Here on the slide we can see that for representation of order's price we use two fields, price and currency. And when we write some business logic, we usually use them separately as primitive types. Instead of using primitive types, domain model patterns suggest to create special immutable class that represents money and encapsulate all the business logic related to it. Now we can use property to use

14:38

order sprice as money value object. But we still can't use this type in ORM queries. And it feels not so native for Django. More native way for Django is to use custom model fields. For example, there is a library that provides money fields. And now we can use money value object in RAM. The pattern makes the code safer and more explicit. Whereas an entity is the opposite of value object, it requires an explicit identification field to distinguish between the different instances of the entity.

15:24

But entity is not used without another pattern, an aggregate. An aggregate is an entity, it requires an ID, and its state is expected to change during an instance's lifecycle. However, it's much more than just an entity. The goal of the pattern is to protect the consistency of its data The public methods that changed an aggregate state are often called commons, and aggregate public interface is responsible for validating the input. and enforcing all the relevant business rules and invariants. Of course, we are implementing commands as methods of the model

16:11

The methods should enforce all invariants to prevent the aggregate from entering an inconsistent state. Since an aggregate state can only be modified by its own business logic, the aggregate also acts as transactional boundary. All changes to the aggregate state should be committed transactionally as one atomic operation. However, sometime group of entities and value objects can be bound by the domain business logic. For example, our students can get an academic leave, but only three times during the education. Sounds like business rule, isn't it?

16:56

In this case, the aggregate pattern resembles a hierarchy of entities, all sharing transactional consistency. On the slide, admission is an aggregate truth and academic leave is an entity inside the aggregate. You can't modify state of non-root aggregates entities directly, because if you do it, aggregate can't guarantee enforcing invariance. So, any non-root aggregate cannot have any methods. All commons must be implemented only in aggregates roots model class. Let's summarize how to deal with aggregates.

17:41

Aggregate acts as a transactional boundary. You can't modify state of non-root aggregate entities directly. The rule of thumb is to keep the aggregates as small as possible and include only objects that are required to be in strong consistent state by the business domain We found that it's very convenient to use Jenga apps as aggregates because in this level implementation aggregate is a set of coherent entities. and an app is a set of coherent Django models. Sooner or later you may encounter business logic that does not belong to single aggregate or value object

18:26

In such cases, domain-driven design proposed to implement the logic as a domain service. The domain service is a stateless class of function that implements the business logic. Let's put it all together. Each bounded context module contains soft services, value objects, and multiple aggregates. Each aggregate is a Jenga app that consists of multiple Jenga models, but only aggregate root model can have methods, or better call it commons. Let's go even deeper. In level 2, we consider the approach that is the best described in this book, and it can be frequently found in various DDD courses and tutorials.

19:21

When the business logic is very complex and the coupling of the database schema with domain model adds additional complexity In such cases, it's recommended to decouple your domain model from the database representation. However, in Jenga, it's impossible to separate the model from the database Therefore, we separate domain model from the Django model and make a domain layer using plain old Python objects. Here we have the coupon aggregate. It doesn't inherit from anything, so it's a plain old Python object. Also note that discount field here is value object. As in level 1, all commands related to the aggregate are implemented as public

20:11

function within this class. To translate the domain model from Django model and vice versa, we can employ the repository pattern The repository serves as an abstraction that shields the domain model from the representation in a persistent storage. But there is some problem in such setup. It violates the dependency inversion principle. Domain layer depends on data access layer, but it shouldn't. Let's invert the dependency. In the domain layer, we establish only the interface of the repository, and the domain model relies on this interface. The data access layer then implements

20:57

this repository depending on the interface. This trick is also known as ports and adapters pattern. The repository interface acts as a port while its implementation serves as the adapter. Here is the example of coupon repository interface. We use typing protocol to define the interface inside the domain layer. Here is an example of Django repository implementation. We implemented each method from the interface using Django models and ORM. This repository depends on the coupon aggregate and Django

21:43

coupon model, and that acceptable for us. Here is the activate coupon service. As I mentioned before, it's a stateless class that implements specific business logic. This class is a part of the domain service. It depends only on the coupon repository interface and not on the implementation. As you can see, testing this service is straightforward without hitting the database. We can simply use an in-memory mock repository implementation. Furthermore, it does not rely on any specific presentation layer views. We can rub this service with an REST type view or gRPC view or any other kind of view

22:32

Finally, now bounded context module is split on three different submodules Presentation contained views, serializers, etc. Domain contained services, plain object aggregates, and value objects. and data access that contains applications that implement repositories and models for storing aggregates to a database. In the second level, there is a greater separation of concerns. The domain layer becomes independent of Jenga. This provides a chance to test business logic without intricate mocks and database hits. However, there are drawbacks.

23:17

It appears unusual to many Django developers. It requires a lot of boilerplate code. Complex ORM queries are not possible since repositories are typically straightforward. The data access layers saves wall aggregate, which slows performance. The frequent translation between Django model and domain model also consumes time. I believe that this approach suits only bounded context with highly complex business logic Of course, you can use level 1 for one bounded context and level 2 for another one in the same Django project. When I mention that

24:02

we are using DDD in our Jungle project, people often associate it with the second level and think we are crazy. However, I have demonstrated that you don't need to make things complex to benefit from DDD. You just need to understand patterns and how to make them work for you in the project. And I hope that after my talk you will understand it a little better Thank you for your attention. I would be glad to answer any questions. Bye

Questions this talk answers

Can Django and domain-driven design work together?

Yes. The speaker argues that Django and DDD complement each other, especially for making large projects easier to develop and maintain through strategic and tactical patterns.

Discussed at 1:10

How do I use bounded contexts to break up a large Django monolith?

Divide the ubiquitous language into smaller languages, assign each to an explicit context, and use those contexts as high-level modules in a modular monolith. A module can be one Django app or a group of apps.

Discussed at 7:27

How do I find the boundaries of bounded contexts?

Use event storming, a collaborative workshop for identifying a domain’s key concepts, events, and processes. The boundaries are strategic decisions that can later be refined by adding or merging contexts.

Discussed at 8:59

What rules should bounded-context modules follow in a Django project?

Each module should own its tables and ideally its database, communicate through DTOs or primitive types rather than exposing internals, and use patterns such as two-phase commit or sagas when cross-module transactional integrity is required.

Discussed at 9:45

How can I avoid an anemic domain model in Django?

Use rich Django models as domain models and put state-changing business operations on them, using terminology from the bounded context’s ubiquitous language. This keeps business rules and state transitions inside the model instead of in views or other external code.

Discussed at 12:08

How do aggregates enforce business rules in Django?

An aggregate root exposes methods—commands—that validate inputs and enforce invariants. The aggregate is also the transactional boundary, so its state changes are committed atomically; non-root entities must not be modified directly.

Discussed at 15:24

What is a domain service, and when should I use one?

Use a domain service for stateless business logic that does not belong to a single aggregate or value object. It can be implemented as a class or function within the bounded-context module.

Discussed at 18:26

How do I separate a complex Django domain model from the database?

For highly complex business logic, represent the domain with plain Python objects and use repositories to translate between those objects and Django models. Define repository interfaces in the domain layer and implement them in the data-access layer, following a ports-and-adapters approach.

Discussed at 19:21

When is it worth using a separate domain layer in Django?

It is most suitable for bounded contexts with highly complex business logic, where database coupling adds significant complexity. It brings better separation and database-independent testing, but also adds boilerplate, translation overhead, performance costs, and limits on complex ORM queries.

Discussed at 23:17

Presenters

Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.

More videos from DjangoCon US