Giter VIP home page Giter VIP logo

ddd-noduplicates's Introduction

Designing a Domain Model to enforce No Duplicate Names

Some design approaches to enforcing a business rule requiring no duplicates.

The Problem

You have some entity in your domain model. This entity has a name property. You need to ensure that this name is unique within your application. How do you solve this problem? This repository shows 11 different ways to do it in a DDD application where the goal is to keep this business logic/rule in the domain model.

Approaches

Below are different approaches to solving this problem. In each case the necessary classes are grouped in a folder. Imagine that in a real application these would be split into several projects, with the domain model in one, repository implementations in another, and tests (or actual UI clients) in others still.

The Database

This simplest approach is to ignore the rule in your domain model (which is basically cheating in terms of DDD), and just rely on some infrastructure to take care of it for you. Typically this will take the form of a unique constraint on a SQL database which will throw an exception if an attempt is made to insert or update a duplicate value in the entity's table. This works, but doesn't allow for changes in persistence (to a system that doesn't support unique constraints or doesn't do so in a cost or performance acceptable fashion) nor does it keep business rules in the domain model itself. However, it's also easy and performs well so it's worth considering.

This repo doesn't use an actual database so the behavior is faked in the ProductRepository.

Approach 1 - Database

Domain Service

One option is to detect if the name is duplicated in a domain service, and force updates to the name to flow through the service. This keeps the logic in the domain model, but leads to anemic entities. Note in this approach that the entity has no logic in it and any work with the entity needs to happen through the domain service. A problem with this design is that over time it leads to putting all logic in services and having the services directly manipulate the entities, eliminating all encapsulation of logic in the entities. Why does it lead to this? Because clients of the domain model want a consistent API with which to work, and they don't want to have to work with methods on the entity some of the time, and methods through a service some of the time, with no rhyme or reason (from their perspective) why they need to use one or the other. And any method that starts out on the entity may need to move to the service if it ever has a dependency.

Approach 2 - Domain Service

Pass Necessary Data Into Entity Method

One option is to give the entity method all of the data it needs to perform the check. In this case you would need to pass in a list of every name that it's supposed to be different from. And of course don't include the entity's current name, since naturally it's allowed to be what it already is. This probably doesn't scale well when you could have millions or more entities.

Approach 3 - Pass Necessary Data into Entity Method

Pass a Service for Checking Uniqueness Into Entity Method

With this option, you perform dependency injection via method injection, and pass the necessary dependency into the entity. Unfortunately, this means the caller will need to figure out how to get this dependency in order to call the method. The caller also could pass in the wrong service, or perhaps no service at all, in which case the domain rule would be bypassed.

Approach 4 - Pass Service to Method

Pass a Function for Checking Uniqueness Into Entity Method

This is just like the previous option, but instead of passing a type we just pass a function. Unfortunately, the function needs to have all the necessary dependencies and/or data to perform the work, which otherwise would have been encapsulated in the entity method. There's also nothing in the design that requires the calling code to pass in the appropriate function or even any useful function at all (a no-op function could easily be supplied). The lack of encapsulation means the validation of the business rule is not enforced at all by our domain model, but only by the attentiveness and discipline of the client application developer (which even if it's you, is easily missed).

Approach 5 - Pass Function to Method

Pass Filtered Data Into Entity Method

This is a variation of Approach 3 in which the calling code now passes just the existing names that match the proposed name, so that the method can determine if the new name already exists. It seems like it's doing almost all of the useful work required for the business rule, without actually doing the check for uniqueness. This to me requires far too much work and knowledge for the calling code.

Approach 6 - Pass Filtered Data to Method

Use an Aggregate with Anemic Children

The problem doesn't mention whether the entity in question is standalone or part of an aggregate. If we introduce an aggregate, we can make it responsible for the business invariant (unique name) by making it responsible for all name changes or additions. Having an aggregate responsible for its invariants, especially when they related between child entities, often makes sense. However, you want to be careful not to have your aggregate root become a god class and turn all of your child entities into anemic DTOs (as this approach basically does).

Approach 7 - Aggregate with Anemic Children

Use an Aggregate with Double Dispatch

Double dispatch is a pattern in which you pass an instance of a class into a method so that the method can call back to the instance. Often it's "the current instance" or this that is passed. It provides a way for an aggregate to allow children to stay responsible for their own behavior, while still calling back to the aggregate to enforce invariants. I prefer to keep relationships one-way in my domain models, so while there is a navigation property from the aggregate to its children, there isn't one going the other way. Thus, to get a reference to the aggregate, one must be passed into the UpdateName method. And of course, there's nothing enforcing that the expected thing is actually passed here. Calling code could pass null or a new instance of the aggregate, etc.

Approach 8 - Aggregate with Double Dispatch

Use an Aggregate with C# Events

This is the first option that introduces using events. Events make sense when you have logic that needs to respond to an action. For example, "when someone tries to rename a product, it should throw an exception if that name is already in use". This approach uses C# language events, which unfortunately require a lot of code to implement properly.

Approach 9 - Aggregate with C# Events

Use an Aggregate with MediatR Events

This approach uses an aggregate combined with MediatR-managed events. To avoid the need to pass MediatR into the entity or its method, I'm using a static helper class. This is a common approach and one that has been used successfully for over a decade (see Domain Event Salvation from 2009). This approach provides one of the cleanest client experiences in terms of API. Look at the tests and notice that all each test does is fetch the aggregate from the repository and call a method. The test code, which mimics real client code in this case, doesn't need to fiddle with any plumbing code or pass in functions or special services or any of that. The methods are clean, the API is clean, and the business logic is enforced in specific classes with that single responsibility.

Approach 10 - Aggregates with MediatR Events

Use Domain Events (no aggregate)

Sometimes it doesn't make sense to involve or model an aggregate. In this case you can still leverage domain events to provide a way to keep logic encapsulated in the entity while still leveraging infrastructure dependencies. The client experience in this approach is very similar to the previous one, with the exception of a bit more client logic being required when adding new products. Otherwise, the client experience is very clean and consistent and not muddied with implementation details leaking from the domain layer.

Approach 11 - MediatR Domain Events

I also have an article walking through how to set this up in your ASP.NET Core app, Immediate Domain Event Salvation with MediatR.

Summary

In most situations where there are business rules that involve multiple entities and their peers, I've found introducing an aggregate makes sense. Assuming you go the aggregate route, be careful to avoid putting all logic in the root and having anemic children. I've had good success using events to communicate up to the aggregate root from child entities (in addition to this sample see AggregateEvents).

If you don't have an aggregate or there would only ever be one and it would have millions of other entities in it (which might be untenable), then you can still use domain events to enforce constraints on the collection as demonstrated in the last example. Using domain events as shown in the last couple of approaches does violate the Explicit Dependencies Principle, but that principle mainly applies to services, not entities, and in this case I feel that the tradeoff of leveraging domain events to provide a clean interface to my domain and keep logic encapsulated in my entities is worth it.

Reference

This project was inspired by this exchange on twitter between Kamil and me. Kamil's ideas for approaching this problem included:

  1. Pass all the current names to the update method. Done
  2. Pass all the names matching the proposed name to the update method. Done
  3. Pass an IUniquenessChecker into the update method which returns a count of entities with that name. Done
  4. Pass a function that performs the same logic as #3. Done
  5. Check the invariant in an Aggregate. Done - Anemic Children
  6. Create a unique constraint in the database. Done

My own two approaches include:

  1. Use a domain service (which will lead to an anemic entity) Done
  2. Use domain events and a handler to perform the logic

Learn More

Learn Domain-Driven Design Fundamentals

ddd-noduplicates's People

Contributors

ardalis avatar julielerman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ddd-noduplicates's Issues

Concurrency considerations

I might very well be wrong, but I think only the database approach will guarantee that the invariant is held in a concurrency scenario. Given a scenario where User A and User B both add a product with the same name "PRODUCT" at the same time, then the invariant check in the other approaches might get bypassed, because the duplicate check query for User A's request is run before User B's request has reached the database, and vice versa (there are no products with name "PRODUCT" in the database when the duplicate check queries run).

Service Locator

I know it's supposed to be an "anti-pattern" but it seems easy enough to just use a service locator. That's what we used to do in our domains and I never remember an issue. It doesn't seem like it would matter if you don't know there is a service checking names, it only matters that the names aren't unique and you need to fix that

Passing the number of users with a similar name to the constructor.

If you receive in the constructor of the User class the number of users with the same email as the int variable. In the constructor, check if it is greater than 1, then throw an exception. In the repository, define a method to get the number of suitable records. What do you think about this?

Async call to event handler

Given the asynchronous nature of the event handler in the last approach (11_DomainEventsMediatR), there is possibility to have the Name change go through the persistency layer before getting the exception and thus creating an inconsistent state. Correct me if I am wrong.

Question about Approach 10 - Aggregates with MediatR Events

Isn't an issue that the Product domain model has public c-tors which do not check for the validity of the name?
This makes it possible to have an invalid Product model.

    public class Product
    {
        public Product()
        {
        }
        public Product(string name)
        {
            Name = name;
        }

        ....
   }

https://github.com/ardalis/DDD-NoDuplicates/blob/master/NoDuplicatesDesigns/10_AggregateWithMediatR/Product.cs

I'm not sure what's the solution for this. Maybe make the Product c-tors private and add a factory method and a new domain event, something like this:

public class Product 
{
       private Product()
       {
       }
       private Product(string name)
       {
           Name = name;
       }

      public static Product Create(int catalogId, string name)
      {
             DomainEvents.Raise(new ProductCreationRequested(catalogId, name)).GetAwaiter().GetResult();
             return new Product(name);
      }
}

Add add the new event handler class inside the Catalog class:

public class ProductCreationHandler : INotificationHandler<ProductCreationRequested>
{
    public Task Handle(ProductCreationRequested notification, CancellationToken cancellationToken)
    {
        var catalog = _catalogRepository.GetById(notification.CatalogId);
        catalog.AddProduct(notification.Name); // AddProduct(catalogId, name) would be private in the Catalog class
        
        return Task.CompletedTask;
    }
}

What do you think?

Pass a private object implementing a public interface representing the unique value to the entity method

Example

Entity method:

public static User Register(
    UserId id,
    IUniqueUsername uniqueUsername,
    IUniqueEmailAddress uniqueEmailAddress)
{ /* ... */ }

Public interfaces:

public interface IUniqueUsername
{
    public string Value { get; }
}

public interface IUniqueEmailAddress
{
    public string Value { get; }
}

Domain service:

public sealed class UserService(IUsersUnitOfWork unitOfWork)
{
    private readonly IUsersUnitOfWork unitOfWork = unitOfWork;

    public async Task<Result<IUniqueEmailAddress>> EnsureUniqueEmailAddressAsync(
        EmailAddress emailAddress,
        UserId? existingUserId = null)
    {
        var isUnique = await this.unitOfWork.Users.IsUniqueAsync(
            new UserByEmailAddressSpec(emailAddress),
            existingUserId);

        if (isUnique)
        {
            return new UniqueEmailAddress(emailAddress.Value);
        }

        return new Error($"A user with the email address '{emailAddress}' already exists.");
    }

    public async Task<Result<IUniqueUsername>> EnsureUniqueUsernameAsync(
        Username username,
        UserId? existingUserId = null)
    {
        var isUnique = await this.unitOfWork.Users.IsUniqueAsync(
            new UserByUsernameSpec(username),
            existingUserId);

        if (isUnique)
        {
            return new UniqueUsername(username.Value);
        }

        return new Error($"A user with the username '{username}' already exists.");
    }

    private sealed record UniqueEmailAddress(string Value) : IUniqueEmailAddress;
    private sealed record UniqueUsername(string Value) : IUniqueUsername;
}

Usage in handler:

public async Task<Result<long>> Handle(
    RegisterUserCommand command,
    CancellationToken cancellationToken = default)
{
    var uniqueUsernameResult = await this.userService.EnsureUniqueUsernameAsync(
        Username.From(command.Username));

    var uniqueEmailAddressResult = await this.userService.EnsureUniqueEmailAddressAsync(
        EmailAddress.From(command.EmailAddress));

    var result = Result.Combine(uniqueUsernameResult, uniqueEmailAddressResult);

    if (result.IsFailure)
    {
        return result.Errors;
    }

    var user = User.Register(
        UserId.From(this.idGenerator.CreateId()),
        uniqueUsernameResult.Value,
        uniqueEmailAddressResult.Value);

    this.unitOfWork.Users.Add(user);

    await this.unitOfWork.SaveChangesAsync(cancellationToken);

    return user.Id;
}

Thanks!

Hey Steve

I just wanted to thank you for this exhaustive list of approaches to the problem - I've found it to be very helpful.

I'm unsure which way I'll go, but at least I have some ideas to consider!

๐Ÿ‘

Another option

Make the id of the aggregate be the username you're trying to create

Duplicates in race conditions between multiple processes

I'm struggling to understand which of these solutions, if any, would work in a scenario where you are scaling up multiple processes that can create or update the same entity type in a document DB. Am I correct that these solutions are targeted at a single-threaded process (or maybe a single process multi-threaded with some added locking code) and not a multi-process solution? Or am I missing something? The DB constraint, which as you said, is too expensive for my case. I have multiple processes that create and store entities in a central document DB and I have a similar business rule to preventing duplicates I need to enforce.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.