2

I'm working on a domain model writing my software all DDD and stuff doing a great job, when I suddenly bump into the same problem I have been facing over and over again and now it's time to share some insights. The root of the problem lies in the uniqueness of data.

For example, let's say we're writing this awesome domain model for a user. Obviously the username is unique and just to be as flexible as possible we want the user to be able to change his name, so I implemented the following method:

public void SetUsername(string value)
{
    if (string.IsNullOrWhiteSpace(value))
    {
        throw new UserException(UserErrorCode.UsernameNullOrEmpty,
            "The username cannot be null or empty");
    }
    if (!Regex.IsMatch(value, RegularExpressions.Username))
    {
        throw new UserException(UserErrorCode.InvalidUsername,
            "The username {value} does not meet the required ");
    }
    if (!Equals(Username, value))
    {
        Username = value;
        SetState(TrackingState.Modified);
    }
}

Again, this is all fine and fancy, but this function lacks the ability to check if the username is unique or not. So writing all these nice articles about DDD, this would be a nice use-case for a Domain Service. Ideally, I would inject that service using dependency injection but this ruins the constructor of my domain model. Alternatively, I can demand an instance of a domain service as a function argument like so: public void SetUsername(string value, IUsersDomainService service) and to be honest I don't see any solid alternatives.

Who has faced this problem and maybe came up with a nice rock-solid solution?

Eduard Keilholz
  • 820
  • 8
  • 27
  • "I'm working on a domain model writing my software all DDD and stuff doing a great job" - nope, not a great job, as this question shows. You try to inject higher end external business logic into a domain model entity, this is NOT doing a great job, it is a fundamental misunderstanding of domain driven design, business services and - essentially - the application of antipatterns. NOT a great job. it is not one user object's function to know whether it's name is unique. – TomTom Nov 05 '21 at 09:19
  • OK, interesting... I thought DDD was all about business decisions and knowledge in one place, the domain model. Now if uniqueness of the username should not be implemented in the domain model, where should it. By the way, I was sarcastic. When I actually was doing a great job, I probably wouldn't end up asking questions here :) – Eduard Keilholz Nov 05 '21 at 10:15
  • What does A (as in a single) user know about the others? Hint: NOTHING. Uniqueness of a name is not a property of a user object. Uniqueness is guaranteed by some sort of user service (or the storage, you can just handle an insert error), NOT the user object itself. There are a LOT of services around, imagine processing a shopping card - the cart is not responsible for checking warehouses etc. and make a shipment plan. This is a typical beginner error, given ENTITY models only you assign too many function to them because you lack non-entity service entities in your model. – TomTom Nov 05 '21 at 10:27

1 Answers1

1

I agree with @TomTom. But as most times with software decisions, it depends, there is almost always a tradeoff. As a rule of thumb, you gain more by not injecting a domain service into an entity. This is a common question when one is starting with DDD and CQRS+ES. And has been thoroughly discussed in the CQRS mailing list here

However, there are some cases where the approach you suggested (known as method injection) might be beneficial it depends on the scenario. I’ll try and drive some analysis points next.

Consider the case where you want to make some validation before creating an entity. Let's think of a hypothetical and way oversimplified international banking context, with the following entity:

public class BankNote
{
    private BankNote() {}

    public static FromCurrency(
        Currency currency,
        ISupportedCurrencyService currencyService)
    {
         currencyService.IsAvailable(currency);
    }
}

I am using the factory method pattern FromCurrency inside your entity to abstract the entity creation process and add some validation so that the entity is always created in the correct state.

Since the supported currencies might change overtime, and the logic of which currencies are supported is a different responsibility than the bank note issuing logic, injecting the ISupportedCurrencyService in the factory method gives us the following benefits:

By the way, the method dependency injection for domain services is suggested in the book: Hands-On Domain-Driven Design with .NET Core By Alexey Zimarev. Chapter 5 "Implementing the Model" page 120

Pros

  • The BankNote is always created with a supported Currency, even if the currencies supported change overtime.
  • Since we are depending on an interface instead of a concrete implementation, we can easily swap and change the implementation without changing the entity.
  • The service is never stored as an instance variable of the class, so no risk of depending on it more than we need.

Cons

  • If we keep going this way we might add a lot of dependencies injected into the entity and it will become hard to maintain overtime.
  • We still are adding a loosely coupled dependency to the entity and hence the entity now needs to know about that interface. We are violating the Single Responsibility Principle, and now you would need to mock the ISupportedCurrencyService to test the factory method.
  • We can’t instantiate the entity without depending on a service implemented externally from the domain. This can cause serious memory leak and performance issues depending on the scenario.

Another approach

You can avoid all the cons if you call the service before trying to instantiate the entity. Say having a different class for the factory instead of a factory method, and make that separate factory use the ISupportedCurrencyService and only then call the entity constructor.

public class BankNoteFactory
{
    private readonly ISupportedCurrencyService _currencyService;

    private BankNoteFactory(
        ISupportedCurrencyService currencyService) 
            => _currencyService = currencyService;

    public BankNote FromCurrency(
        Currency currency)
    {
         if(_currencyService.IsAvailable(currency))
             return new BanckNote(currency);
             // To call the constructor here you would also need 
             // to change the constructor visibility to internal.
    }
}

Using this approach you would end with one extra class and an entity that could be instantiated with unsupported currencies, but with better SRP compliance.

dave_077
  • 83
  • 8
  • BTW I just found a very interesting DDD-oriented article that solves this using double dispatch patterns in C#: Double dispatch in C# and DDD: https://ardalis.com/double-dispatch-in-c-and-ddd/ – dave_077 Sep 19 '22 at 21:12
  • Careful when using Factories. They may be a bit more complex. https://stackoverflow.com/questions/13804765/factory-pattern-where-should-this-live-in-ddd/74146382#74146382 – Pepito Fernandez Oct 20 '22 at 22:01