2

I am developing an small application, just to practice DDD. Afaik. invariants are the umbrella term of validation related to domain. So for example if I want to have only ucfirst names, then that is an invariant and I need validation in the property setter to make sure that this invariant is protected and make only the setter public, not the property itself.

My current problem is that I need to have usernames, which are generated from firstname, lastname, birthDate and probably some random stuff, if these are not unique enough. As far as I understand having a unique username is a domain concern, because it is very important if you want to login, or just bind some data to an actual user. So having a database table with pk on the username and wait for violating constraint error is just not enough, I should put some code into the domain, which makes sure that the generated username will be unique. I guess the code goes into the repository or into the entity somehow. Can you show me an example how this would look like in a real app? The domain and the app service implementation what I am curious of.

I might not be right about this. Every entity has a unique identifier and has repository, so probably I should put the username generation into the repository, and that's all. I might need to rephrase this for example what if we don't have a unique identifier, just a property which should be unique?

I made a little code draft:

class CustomerService {

    // ...

    public createCustomerFromNameAndYear(name, year){
        try {
            transaction.begin();
            username = generateUsername(name, year);
            while (repo.findByUsername(username))
                username = addSomeRandomChar(username, 1);
            password = "";
            password = addSomeRandomChar(password, 6);
            customer = new Customer(username, password, name, year);
            repo.save(customer);
            transaction.commit();
            dto = new CustomerDTO(customer.getUsername(), customer.getPassword());
            return dto;
        }
        catch(exception) {
            transaction.rollback();
            throw new ExceptionWithCauses(CUSTOMER_CREATION_FAILED, exception.getMessage());
        }
    }

}

If I put this into the app service, it will ensure that the name is unique unless if concurrence happens, but sending an error message is okay in that rare case. I am not sure whether this is okay. It does not feel right if the invariants must be parts of the domain code.

Another possible solution to have a create method in the repo.

class CustomerService {

    // ...

    public createUserFromNameAndYear(name, year){
        try {
            transaction.begin();
            customer = repo.create(name, year);
            transaction.commit();
            dto = new CustomerDTO(customer.getUsername(), customer.getPassword());
            return dto;
        }
        catch(exception) {
            transaction.rollback();
            throw new ExceptionWithCauses(CUSTOMER_CREATION_FAILED, exception.getMessage());
        }
    }

}

class CustomerRepository implements iCustomerRepository {

    // ...

    public create(name, year){
        username = generateUsername(name, year);
        while (this.findByUsername(username))
            username = addSomeRandomChar(username, 1);
        password = "";
        password = addSomeRandomChar(password, 6);
        customer = new Customer(username, password, name, year);
        this.save(customer);
        return customer;
    }
}

This might be better.

  • 2
    I don't see any transaction management in your Application code. The most important role of the Application layer is to wrap the domain code in a transaction. – Constantin Galbenu Feb 14 '17 at 18:23
  • 1
    Both of your solution permit (in rare cases though) that two users have the same username, so it is buggy. Think about concurrency in the worst scenario – Constantin Galbenu Feb 14 '17 at 18:31
  • @ConstantinGALBENU Yes, I know, I skipped that part, the original draft did not contain classes and methods. – inf3rno Feb 14 '17 at 18:34
  • 1
    Why can't you wait for a constraint violation? Is not that a good enough indication to continue trying other logins? – Basilevs Feb 14 '17 at 18:59
  • 2
    In my last project I used an dedicated aggregate UsernameAllocation that dealt with unique usernames. I had a requierement that the user must choose another username in case of conflicts. – Constantin Galbenu Feb 14 '17 at 19:02
2

In a multi-tier application an application is usually divided into three layers:

  • presentation,
  • business logic,
  • data access.

A data access layer should do one thing and do it well, it should know how to retrieve and persist entities which are used by business logic layer and shouldn't contain any business logic.

Note, that I am saying shouldn't, because errors in your data access layer may be your last resort mechanism, when everything else failed - such as you're trying to insert a duplicate record of a unique attribute because you failed to check beforehand.

But why exactly would a unique username attribute exist within your system? There are two posibilities:

  1. The data access layer's mechanism produces this constraint and you have no control over it.
  2. Your stakeholders told you that in your system a username MUST be unique and will be used for signing in - stakeholders presented a business rule which must be fulfilled.

We can completely ignore case number 1, as data storages do not care at all what goes into them, as long as the format is correct they do not care about the actual value. They only limit you to unique values if you tell them to do so. Meaning, you are actually facing the 2. situation - a business rule.

With the rule in mind, you added a constraint to your data store engine (should it support) that an attribute MUST indeed be unique, but as I have mentioned, you shouldn't rely on this, it's only there to fail at the lowest level when everything else fails.

And because the rule that a username MUST be unique was actually produced by the business owners, it belong to the business logic layer, that's its rightful place.

Your repository create method should only contain the INSERT command and no checks anymore. And if someone forgets to check for unique username and uses the create method of the ICustomerRepository interface without checking for a unique username then it's expected the repository method might fail, because the programmer ignoring the check actually broke an important rule.

  • Thank you, then I was right that this is domain related. :-) I'll wait, maybe Constantin writes an answer too. :-) – inf3rno Feb 14 '17 at 19:50
  • BTW there could be no way for the domain-level code to detect a duplicate, when two sessions create duplicate records without yet committing them to the data store, then sending the data simultaneously. The storage layer will detect the duplicate and report the problem. The layers above it should be able to reasonably handle the situation. – 9000 Feb 14 '17 at 22:21
  • 1
    @9000 Yes, they should, but as I have mentioned it should be your last resort solution, not the first approach you should be taking. – Andy Feb 15 '17 at 6:28
  • So if this is domain model logic you shouldn't use/rely on the uniqueness constraint in your database? And the domain code for creating and updating usernames should be mutually exclusive and enforce uniqueness? What about someone just batch inserting new and duplicate usernames directly into the database bypassing domain model logic? – Wouter Mar 29 at 12:23
0

There are multiple solution to this problem. I will give one that imply the creation of an specialized aggregate who's responsibility is username allocations. The language is PHP, but I hope that is generally understandable.

So the ideea is that the UsernameAggregate is verifying that the username uniqueness invariant is respected. It accepts the command AllocateUsername to allocate an username and raises an event or throws an error. This command is dispatched before the command RegisterNewUser is dispatched, in the Application layer.

class UsernameAggregate
{
    private $allocatedUsernames = array();

    public static function getAggregateId():Guid
    {
        //singleton aggregate
        return Guid::fromFixedString(self::class);
    }

    public function handleAllocateUsername(AllocateUsername $command)
    {
        if ($this->isUsernameTaken($command->getUsername())) {
            throw new \Exception("This username is already taken");
        }

        yield new UsernameWasAllocated($command->getIdUser(), $command->getUsername());
    }

    public function applyUsernameWasAllocated(UsernameWasAllocated $event)
    {
        $this->addUsername($event->getUsername());
    }

    private function isUsernameTaken(string $username):bool
    {
        return isset($this->allocatedUsernames[$username]);
    }

    private function addUsername(string $username)
    {
        $this->allocatedUsernames[$username] = 1;
    }
}

In the Application layer:

public function registerNewUser($username, $userId)
{
    $this->commandDispatcher->dispatchCommand(new AllocateUsername($username));
    $this->commandDispatcher->dispatchCommand(new RegisterNewUser($userId, $username));
}

If the username is already taken, then an exception is thrown and RegisterNewUser command never gets dispatched, thus enforcing the invariant. In my code, CommandDispatcher is responsible for loading the aggregate, command dispatching and saving the events to the event store, in a transactional way (source) .

  • @plax The point is that the event is persisted in the event store and the second atempt to allocate the username would rehydrate the aggregate with this event and then thrown an exception. This is basic Event Sourcing. – Constantin Galbenu Feb 15 '17 at 5:16
  • 1
    I don't think I understand the design. :S Probably I don't have the skills. Wouldn't it be better to put this into a domain service? Can you write something about why this singleton aggregate is the best solution here? – inf3rno Feb 15 '17 at 8:25
  • 1
    @inf3rno I like my domain services to be stateless – Constantin Galbenu Feb 15 '17 at 9:28
  • You added an entire service/event bus to check whether a username is available, which could have been simply done using a single select? You overengineered the solution of a very simple problem. Don't use concepts just because they're cool. You should KISS. Not to mention, you current solution requires to load all currently used usernames into application memory, that just does not seem right. – Andy Feb 15 '17 at 17:01
  • @David Packer the OP asked for a solution like this. As I said, there are multiple solution. I could think of at least 2 solutions that are faster/lighter. – Constantin Galbenu Feb 15 '17 at 17:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.