I recently tried a code kata that Steve Smith posted in our devBetter Discord server. The kata is essentially a modelling exercise. The goal is to model a collection of value object items, where the collection maintains a “default” item. It forces you to consider things like
- How do you handle selecting which item is the default?
- How do you avoid the possibility of the collection ending up with no default item?
- How do you handle the case where the default item is removed from the collection?
- How do you avoid the possibility of the collection having no items at all?
I approached the task very simply. I created a DefaultItemCollection
class with a DefaultItem
property and a List
of items. I then added some methods to deal with some of the scenarios mentioned above. Here’s the code:
public class DefaultItemCollection<T>
where T: IEquatable<T>
{
public T DefaultItem { get; private set; }
private List<T> _items = new();
public IEnumerable<T> Items
{
get => _items;
}
// Other methods omitted for brevity
}
It’s not perfect, and I’m sure there might be some edge cases I didn’t think of, but it mostly satisfies the requirements of the kata.
Afterwards I was curious to see how others approached the problem. Steve directed me to one of Fati Iseni’s excellent blog post that covers that exact topic. In the post, Fati describes three different approaches to the problem, and two of those approaches are significantly different from the one I took.
Whereas my solution is generic, the example Fati uses is that of a Customer
entity, which possesses a collection of Address
. Aside from that, we really are dealing with the same problem.
The first solution presented, which is described as “simplest and most straightforward,” is to modify the type of the items in the collection to include a bool IsDefault
property.
public class Address
{
public int Id { get; private set; }
public string Street { get; private set; }
public string City { get; private set; }
public bool IsDefault { get; set; } = false; // <- new property
public int CustomerId { get; private set; }
public Customer Customer { get; private set; }
// Other properties and methods omitted for brevity
}
public class Customer
{
public int Id { get; private set; }
private readonly List<Address> _addresses = new List<Address>();
public IEnumerable<Address> Addresses => _addresses.AsEnumerable();
// Other properties and methods omitted for brevity
}
To me, right off the bat, that seemed more complicated than my solution. I’d rather have a specialized collection that maintains the default item than have to modify the type of the items in the collection. Fati does a good job of pointing out the problems with this approach. But what struck me is that what he described as the “simplest and most straightforward” solution wouldn’t even have occurred to me. Why did his mind go there first, and not mine? We’ll get back to that in a minute.
The second solution looks a lot like mine.
public class Customer
{
public int Id { get; private set; }
private readonly List<Address> _addresses = new List<Address>();
public IEnumerable<Address> Addresses => _addresses.AsEnumerable();
public Address DefaultAddress { get; set; }
// Other properties and methods omitted for brevity
}
Fati adds a DefaultAdress
property to the collection, and then adds some methods to deal with the scenarios I mentioned above. Great, we’re done! Nope. Fati points out that this solution is also problematic. He then proceeds to discuss persistence, 1:n relationships, 1:1 relationships, foreign keys, circular dependencies and problems that could happen if you tried updating the DB manually. As I’m reading this I’m thinking, “What the heck? How did a simple collection with a default item property turn into such a complicated thing?” We’ll get back to that in a minute too.
Fati moves on to the third solution, which he feels is “quite appealing.”
public class Address : ValueObject
{
public string Street { get; private set; }
public string City { get; private set; }
// Other properties and methods omitted for brevity
}
public class CustomerAddress
{
public int Id { get; private set; }
public Address Details { get; private set; }
public int CustomerId { get; private set; }
public Customer Customer { get; private set; }
// Other properties and methods omitted for brevity
}
public class Customer
{
public int Id { get; private set; }
private readonly List<CustomerAddress> _addresses = new List<CustomerAddress>();
public IEnumerable<CustomerAddress> Addresses => _addresses.AsEnumerable();
// This is owned type, not a navigation.
public Address DefaultAddress { get; private set; } = Address.Empty;
// Other properties and methods omitted for brevity
}
The solution is to create a wrapping entity class around the value object item that would link the items of the collection, to the collection. Why do we need that extra layer? I’ll tell you why: Relational databases.
And that’s when it dawned on me. Fati, like many other developers, when modelling a problem in code, is thinking about how everything is going to get persisted in a relational database. And honestly, I can’t say that that is a bad thing. It certainly seems pragmatic. But a part of me can’t help being a little “grossed” out by this.
My own solution, which is similar to Fati’s second, is fairly simple and elegant. It seems to me like the solution most anyone would come up with if they didn’t have to worry about persisting the data. If all they had to worry about was the code, the domain and the business logic. Within the DDD corpus, you are generally encouraged to think about the domain first, use value objects as much as possible, and essentially compose trees of entities and value objects. When you think about it, the ideal DDD model, in code, is quite different than the ideal relational database model. Which leads to things like Fati’s third solution, which is a lot more complicated than the first two.
I’m not saying that Fati’s third solution is bad. It’s not. It’s a perfectly valid solution to the problem. But it’s not the solution I would have come up with if I didn’t have to worry about persisting the data in a relational database. And I’m not saying that Fati’s first solution is bad either. It’s not. It’s also a perfectly valid solution to the problem. But it’s not the solution I would have come up with if I didn’t have to worry about persisting the data. To me, the natural, code first, DDD first solution is closer to Fati’s second solution. And that’s essentially the solution I did come up with because I didn’t need to persist the data in a relational database for this kata.
But then this made me think. Are we forever doomed to bastardize our nice, simple, DDD models, just so we can persist them in relational databases? Is there a way to have our cake and eat it too? Is there a way to have a nice, simple, DDD model, that is also easy to persist in a database?
I’m not entirely sure. But I can tell you one thing, to me, compared to relational databases, document databases model things in a way that is much more similar to how I would model things in code, using DDD. And I think the reason why I don’t tend to first model things like Fati and many other developers do is that my first introduction to databases wasn’t relational databases. My first introduction to databases was document databases, like MongoDB.
Because of that, it feels to me like a DDD project would often benefit from having a document database as the persistence layer, instead of a relational database. Document databases like MongoDB have come a long way and many of the arguments against them and in favour of SQL databases are no longer valid. Or at the very least, have been reduced significantly.
When we code, we often think about mantras and heuristics like “Keep it simple stupid,” “Don’t overengineer” and “Make it work. Make it right. Make it fast.” It seems to me like starting off a DDD project with a document database, instead of a relational database, might be a good way to apply these. I don’t know about you but the idea of not having to invent intermediary wrapping entities (that don’t really have a place in the DDD model) around my value objects just to persist them in a database sounds pretty appealing to me.