At which point does a debate about a piece of code become far too expensive in time?
Merci à Jason Tan pour sa révision de mon anglais qui n’est pas prod ready :)
At which point does a debate about a piece of code become far too expensive in time? It is hard to tell, but here is an oversimplified summary of a discussion about DDD that took more than an hour involving 3 people. Feel free to share your similar experiences :)
Dev1: Hey look at my code, I have a well-defined Client aggregate root class. And instead of having a list of CreditCards, I wrapped the concept in a Wallet value object class and used it inside my Client class. This way, I can manage all the boilerplate code related to handling the list inside the Wallet and keep the Client class clean.
Dev2: Nice, I love it when we use this pattern. But how did you handle the invariant that you cannot have credit card duplicates inside your wallet?
Dev1: Wallet to the rescue! I added this validation in it, and I throw an exception if it happens.
Dev2: But when you will investigate your log file, you won’t be able to know for which client it crashed and you won’t be able to troubleshoot it correctly since you throw the exception from a Wallet.
Dev1: Oh, you’re right. I’ll fix it.
*** Minutes later ***
Dev1: Well, it’s not easy to fix… I don’t want my Wallet to own its parent identifier, the ClientId, because it defeats the purpose of having a value object for that. I could throw a decontextualized exception from the Wallet, catch it inside my aggregate root Client, and then rethrow an exception including the ClientId. Or I could include the ClientId in all my calls to the Wallet, and then throw a full contextualized exception from the Wallet. None of these options are clean. I think we’ve had this same kind of discussion five times this year and each time, we were not satisfied with the result.
Dev2: What if we put the invariant at the wrong place? I think the invariant is “we cannot have duplicate cards for a single Client”, not just “we cannot have duplicate cards”. I think we should place the invariant inside the aggregate root Client. Imagine if we add another invariant saying that a Client can only have a single credit card for each credit card type; for example, you can have a Visa and a MasterCard, but not two Amex.
Dev1: Interesting. So you are saying that the Wallet should be free of those invariants, and only handle the list of credit cards. And then, the Client would be responsible for inspecting the Wallet before adding new cards in it because it is the Client who owns the invariant. Interesting. But there is something wrong with that. Everywhere the Client would like to pick the only Amex card in the Wallet for example, it would need to GetAllCreditCardsOfType on the Wallet (or AllCards), and then use the List.Single card from the list. The Wallet value object is losing all its benefits. I prefer to keep the invariant inside the Wallet.
Dev2: Yeah but imagine we have a funky business rule saying that if a Client has been created before 2017, it can own two Amex cards. It proves the invariant cannot be in the Wallet. Oh God I am so good!
Dev1: (silence)
Dev1: You are right. I don’t know, I still feel the Wallet encapsulation would be awesome.
Dev2: Yeah I know, me too. But you know, a Wallet is always contextualized. It is always in the pocket of someone. We cannot just add some invariants like that in the Wallet and pretend we can use this value object outside the aggregate root. It won’t be reusable.
(silence)
Dev2: What if we model a Policy concept and we include it in the Wallet constructor. This way, the Wallet will always carry its invariants, but would have been configured by the owner! Oh God I am so good!
Dev1: Interesting. But what happens if two Wallets own the exact same credit card, but with different policies. Since they are value objects, they won’t be equal. Value object equality is based on their attributes, not their behavior. And Policies look like behavior, no?
Dev2: Damn. Why is all this so complicated? I don’t know, man. Do it as you want. Anyway, the aggregate root will still be a clean boundary, so it is okay.
Dev1: You are right. Bye.
Dev2: Bye.