Creating an Inclusive Django Community with Kenya Phelps
Published July 15, 2026
This video features Pavel Sviridov at DjangoCon US 2023 in Durham, North Carolina, USA.
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.
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.
Summarised automatically from the transcript.
Automatically transcribed, so expect mistakes in names and technical terms.
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
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.
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.
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
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.
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
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.
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
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.
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
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.
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
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
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.
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.
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.
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
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
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.
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
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?
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.
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
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.
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
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
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
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
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.
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
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
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:10Divide 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:27Use 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:59Each 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:45Use 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:08An 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:24Use 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:26For 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:21It 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:17Note: 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.
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 14, 2026