Where to put code related to invariants

domain-driven-designinvariants

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.

Best Answer

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.

Related Topic