Antipatterns in Domain Driven Design
Podcast: Play in new window | Download (55.1MB) | Embed
Subscribe: Apple Podcasts | Spotify | Email | RSS | More
Per wikipedia, “DDD is the concept that the structure and language of software code (class names, class methods, class variables) should match the business domain.” Essentially the idea is that the code and the business logic that it implements will have a shared vocabulary that makes communication between the developers and the stakeholders easier by moving implementation details out of the code that handles business logic. The primary focus of the application is on the core domain and its domain logic.
Designs are based on a model of the domain, rather than on a model of the implementation details (such as database structure) that is required to support the domain. This approach allows rapid iteration involving both the domain experts and the development team. It also makes it quicker and easier to onboard developers, as they can look at the code to understand business rules.
DDD has a few core principles that are observed. Until you really get very deep into, a useful way to think about it is that it is the application of OOP to business rules. While this is not entirely accurate, it is how you end up with most of the anti-patterns we are about to discuss. Most of these problems really arise because developers have halfway implemented DDD, but haven’t taken it as far as they need to in order to be able to reap all the benefits.
If you work for a company that is trying to use DDD, but hasn’t quite internalized all the lessons of it, you’ll probably run into one or more of these anti-patterns as you go through their codebase. It’s important to remember that they at least tried and got part of the way. You simply need to get them the rest of the way to successfully using this excellent approach to building software.
Primitive obsession occurs when language primitives (such as strings, integers, etc.) are used instead of small objects for simple tasks. Because the validation and rules around the type aren’t sufficient for your use case, you’ll have to add more rules that live outside the object. A good example of this is using a string for an email address. While email addresses are definitely strings, not all strings are email addresses. This makes it possible for an invalid email address to be set, and also means that you have to keep the validation around email addresses somewhere else. Create custom types (often value objects) to encapsulate the types you are trying to express in code. Include validation logic with the type. You will likely have to make conversions between the primitive type and your type at the edges of your system. This would include API endpoints, database, and file interactions which are not part of your core business rules.
Anemic domain models
An anemic domain model occurs when entities are built as simple bags of readable properties. This forces the entity logic into other locations in the code. The entities define little or no behavior themselves and can be put into a bad state from outside themselves. An example of this might be a Person object with name, address, phone number etc. on it. If someone wants to change the address, they either interact directly with the properties on the object itself, or make a call to an object that does it for them. This could mean that the object ends up in an invalid state, for instance, if the city in the address was changed, but the postal code was not. Remove setters on public properties, then create methods for discrete actions on the entity to replace what callers were doing. Alter the callers to use these methods. Get rid of empty constructors and create parameterized ones that initialize the entity in a valid state. You’ll probably find that some callers are difficult to refactor, because they orchestrate changes over a set of entities. You may also have issues around constructors if your framework of choice initializes objects for you and expects empty constructors.
Entities in a system should know their identifiers. Objects that need to retrieve those entities should know about the IDs. Other entities should have references to the entity instead of the ID. Otherwise they will be forced to interact with the persistence layer when they need the entity. For instance, let’s assume you have a Invoice entity that contains a set of line items, each of which have an InventoryItemID on them. If you are trying to determine how much the entire order weighs, you’ll have to pull in the InventoryItems by their IDs, which means that your entity layer is now overly coupled to your persistence layer. Replace ID references with references to the actual entity. Lazy load if necessary for performance. This can be interesting if the object being referenced is large or takes a lot of time to load, as it can cause performance issues. You may have to slightly bend the rules of DDD in order to get good performance.
Entanglement between commands and queries
The concerns between the “write” side of an application and the “read” side of an application are not the same. The command (write) side probably needs access to the entire entity model. The query (read) side only needs parts of the entity and related entities. If you entangle the two sides, you’ll either have very slow reads due to bringing back too much data, or your writes won’t have all the data they need. Let’s say that you have the Invoice and LineItem entities in a model as before, with the LineItems also having a reference to the InventoryItem to which they are tied. If you have the command and query side entangled, a query to get the invoice totals for the last year means loading EVERYTHING under the invoices, even if that doesn’t make sense. Queries should generally not load up the entity model. They are simply a projection off of the entity model that is tailored to what is required by the caller. This can collide in a bad way with the way many developers misunderstand DRY. Devs will want to reuse the code that loads an entity up for query purposes, even though these are two entirely disparate reasons for loading the data. This will tangle your command and query infrastructure up in a bad way and hurt performance.
Lack of bounded contexts
Sometimes an entity is used in very different ways by different parts of a system. An Order entity, for instance, looks very different from the Inventory side of things than it does for fulfillment or billing. A naive implementation might tie these two concepts together in such a way that a change in either area impacts the other negatively. Let’s assume you have an Order entity that is used by your warehouse inventory system, your fulfillment system, and your billing system. Your billing system and warehouse management system change due to expanding your company into other countries, and your billing system is forced to change due to tax law changes. If it’s all the same entity, it’s all the same project, which could mean expensive compromises on your warehouse/fulfillment systems due to regulatory compliance issues on the billing system. Use bounded contexts so that a multi-faced entity is broken into multiple logical entities for each of the business contexts you are trying to handle. Keep their functionality separate. This can be brittle if your organizational structure is still unstable, or if you had a single context entity that got huge and entangled. The latter is especially nasty politically, because it’s hard to show a business value for rewriting working code until that code is already costing money by causing problems. By then, you are getting blamed for the problems.
Implementation driving the domain model
Your domain model should be broken up based on the business model, not based on the database structure. This may mean that a single entity spans multiple tables, that inheritance is in the mix, or even that attributes are loaded/saved from disparate data sources. In a fulfillment system you have an Order entity, but there are two subtypes, OnlineOrder and PhysicalOrder, which refer to how those orders are processed and delivered. However, because you have multiple tables in the mix, the OnlineOrder and PhysicalOrder are separate types with references to their base Order, which represents another table. Essentially this is an active record model. The model structure cannot change independently from the persistence structure. Separate storage from the entities being stored. You might have some that belong to a single table, and you might not, but the table structure is completely irrelevant. This can be interesting with some ORMs because they do a poor job of handling relationships within a single entity, especially with inheritance in the mix.
Exposing entities at the edge
Your domain model should live in an inner ring of your app and shouldn’t go outside of it. You have an order type, with line items, etc. under it. An advisory is suddenly required by law for certain kinds of items in certain contexts. If you are exposing the full entity at the edge, this change will break the interface contract for callers. Convert entities to DTOs before sending them over the wire to a caller or to another system. This gives you another layer where you can decouple your implementation from that of the client. If you do this, be sure and have good testing around the conversion to (and possibly FROM) DTOs, as this is another place where you can easily forget something important.
Transit of invalid state
An entity should always be in a valid state, never an invalid one. This includes just after deserialization, construction, and while changes are being made to it. Invariants should be enforced across the lifetime of the object. You have an order entity (noticing a pattern here?) and your system expects the Total value to never be negative. Due to adding coupons to the system, however, a situation arises where the order total is negative. Rather than being caught, the errors are saved to the database and only caught 20 days later when the accountants notice a discrepency in the books. Don’t have public setters. Always have methods that change state and coordinate the changing of multiple properties, based on logical criteria. For instance, in the previous method, rather than simply adding a coupon item with a negative value to decrease the total, you might create an ApplyCoupon method that applies a coupon up to the point where the total is zero. This sounds easy to accomplish, but sometimes ORMs will get in your way due to constraints on constructors. This can also be an issue if older records were valid under a certain set of conditions and newer ones are not. You will have to figure out how to mitigate this.
Mutable value objects
Like entities, value objects should enforce their invariants during their lifetime. However, because they may be passed around among entities, they also shouldn’t support changes in state, as that adds a lot of low value complexity. Imagine a situation where you have a system that has a directory of people with their addresses. You also keep track of marriages. When someone gets married, your system has a place to match the spouse’s addresses. If Address is a value type and you allow its contents to be changed, you either risk the spouse addresses getting out of sync or you have to write a bunch of code to keep them in sync. Extract reusable sets of fields into value types. Make them immutable. Always point to the same instance when you mean the same set of values. This can be tricky if you are using an ORM that doesn’t support immutable value types, because you may get multiple instances for the same thing. However, if your language allows you to override the equality operator, you may be able to get around the problem by simply comparing the contents.
Tricks of the Trade
A lot of times we get upset at the wrong person or thing when something negative happens. Getting mad at a host because a restaurant is full or at your laptop because the Windows update broke something is an anti-pattern of problem solving. Instead of getting frustrated or acting out look for a solution to problems. It’s not easy, especially when emotions get involved so start just with recognizing that you are upset and then work to identify the source of the problem.