2

In discussion about an architecture decision that we do consider wrong and how exactly to address it, some lack of understanding on the matter arose.

In dealing with authentication and authorization, the decision to move it to outside our domain libraries has proved to be more difficult than at first glance it seems.

I could describe a very long list of issues we may antecipate, I'm not going to do so has it seems unlikely for someone to address all of them or at the very least is unfair. Instead I will pose the basis of the problem and ask for comments from someone more experienced in here.

The context for the problem is as follows.

The domain approach

The domain approach is one the is very familiar to us, hence easy to understand. But this approach poses some questions and those questions pose some challenges we're having a bit of trouble addressing, conceptually.

from dataclasses import dataclass
from uuid import uuid4

@dataclass
class User:
  id: uuid4
  name: str

  @classmethod
  def register(cls, name: str) -> 'User':
    return cls(uuid4(), name)

One could define a user domain entity as shown above. This looks familiar since it is pretty much the way every other scenario works in the application stack.

We would fool ourselves saying we have users and rules to register them.

Use cases

The whole interactions one could do with a user are then handled by one or more use cases. Of which they use a gateway abstraction to communicate with some persistance mechanism.

from abc import ABC, abstractmethod

def register_user(gw: 'RegisterUserGateway', name: str) -> dict:
  if gw.is_name_registered(name):
    return {
        'success': False,
        'error': 'User name already in use.'
    }

  user = User.register(name)

  gw.save(user)

  return {'success': True, 'user': _user_dto(user)}

class RegisterUserGateway(ABC):
  @abstractmethod
  def is_name_registered(name: str) -> bool: pass
  @abstractmethod
  def save(user: User) -> None: pass

def _user_dto(user):
  return {
      'id': user.id,
      'name': user.name,
  }

This very much, as far as we understand, complies with all principles of a clean architecture. Some factors we do consider as benefitial in the long run of the application, and specifically when dealing with interface adapters:

  • Controllers communicate with the application layer by means of cleary defined data structures. Use cases accept a defined set of primitive parameters.
  • Presenters deal with dictionaries containing the data they will need to present, also following the same principle.
  • Gateway implementations have the same principle enforced, this time by means of domain entity class constructors and domain entity class attributes to access data later required for instantiation.

So boundaries are well defined.

This as proved to be a fundamental benefit of this architecture, and most likely the single biggest contributor to the confusion that I'm about to describe.

The issue arrises when one figures out that user registration, authentication, access recovery, and the whole deal that comes with it, is an application concern. At that point I do have a very hard time considering user a domain entity.

The application approach

When we take the observation above and try to rearchitect authentication as an application rule alone we are facing some design issues.

Consider the same use case, but without domain entities involved.

Use cases already have a clear definition of data transfer objects, parameters for request data, dictionaries for response data. So we could simply use the _user_dto function as a basis for the boundary data model.

from abc import ABC, abstractmethod

def register_user(gw: 'RegisterUserGateway', name: str) -> dict:
  if gw.is_name_registered(name):
    return {
        'success': False,
        'error': 'User name already in use.'
    }

  user = {'id': uuid4(), 'name': name}

  gw.save(**user)

  return {'success': True, 'user': user}

class RegisterUserGateway(ABC):
  @abstractmethod
  def is_name_registered(name: str) -> bool: pass
  @abstractmethod
  def save(id: 'uuid4', name: str) -> None: pass

This is seems like a coherent approach. The benefits we do consider as fundamental are still kept.

But some questions obviously arise:

  • The gateway interface is not using a domain entity anymore. Is this an issue later down the road?
  • The use case itself is now responsible for id generation (and format). Is this not matter of the domain layer?
  • Some other aggregates need to know about this new user, and a domain event seems not appropriate. Does this mean we should call other use cases where we previously raised domain events?

And again I'm not looking for answers to those specific questions, other questions will arise; unless you believe its important to do so. We are just unsure of drawbacks that may haunt us in the future with such a change. Any comments on this matter are very much appreciated.

Thanks.

Note. The code samples provided are meant to help in describing a conceptual problem and have no relation with actual system code.

7
  • The actual problem statement seems to be buried right in the middle of the question, and not articulated very clearly (the paragraph starting "The issue arises when one figures out that ..."). What exactly do you mean by user authentication being "an application concern", and how does this lead to the conclusion that there should not be an entity to represent it?
    – IMSoP
    Commented Jul 6, 2020 at 16:02
  • @IMSoP thanks. I mean exactly that. It is not a domain concern, not a use case concern, its an application concern, in layered/clean architecture. Use cases need ids to fetch buyers. Buyer is a domain entity and that is all the use case cares about. Buyer needs an id, the application sends a user id, thats how we get buyers. Take a read at good explanation of the fact softwareengineering.stackexchange.com/a/264846/351780 Commented Jul 6, 2020 at 16:19
  • @IMSoP If authentication is an application concern and there is no concept of a user at the domain level, then a user entity is a violation of the dependency rule. That is the source for the conclusion. You can wrap any library around an authentication, does that library need to know that? The library is our domain and at moment it does know there is at least one application that has authentication. There are others that don't have it (cron jobs for example). Commented Jul 6, 2020 at 16:24
  • As a result of feedback, I want to clear a confusin quickly. Entities, in our sense, are objects that have ids. What I meant was domain entity in most cases. Commented Jul 6, 2020 at 16:54
  • 1
    I still don't really understand the question. Why does something not being in the domain layer change what capabilities it can have? Why is "generating an ID" responsibility of a particular layer? What does it mean for parts of the domain to need to know about a new user, if they don't know "user" is a concept that exists? Isn't the point about layers that "the concept of user" resides in that layer, not that it has to be implemented a particular way?
    – IMSoP
    Commented Jul 6, 2020 at 16:58

1 Answer 1

1

In a comment you linked to another question which references an article, where the user is placed in a separate "use case layer" rather than the domain layer, but unlike in your example the user is still an entity:

The use cases layer for our shop mainly consists of a User entity and two use cases. The entity has a repository, just like the entities from the domain layer, because users need to be stored to and loaded from a persistence mechanism.

This seems to be the fundamental confusion in your question: you have assumed that entities can only exist in one layer of the application, but there's no reason that needs to be the case. The only restriction is that layers can't see details which belong to outer layers; how they handle their own responsibilities is a separate question.

Within the layer that's responsible for authentication, User is a valid entity - you can't describe a password reset flow without the password belonging to something. That entity must not be passed into the Domain layer; but it can reference anything from the Domain layer, e.g. linking a User to a particular Customer as defined by business rules; and it can be referenced by higher layers, e.g. to implement the UI for the relevant use cases.

A different layer wrapping the same domain library might have a completely different definition of User, or none at all. Take a batch processing application - it might need to track the contact details of the user who should be notified on completion, but not have any interactive authentication. On the other hand, it might need to track the progress of different batches, so have its own entity to represent that state.

So my answer to your examples would be:

The gateway interface is not using a domain entity any more. Is this an issue later down the road?

On the contrary, it is best for abstractions to only cross one boundary at a time. If a higher layer of the application has access to the User entity, it should be consistently using it, not by-passing it and accessing the domain layer directly.

The use case itself is now responsible for id generation (and format). Is this not matter of the domain layer?

Why should it be? If the user only exists in one part of the application, then no other part needs to know that an ID even exists. If the ID needs to be managed by the domain layer, it must belong to something that exists at that layer - e.g. a Customer - and should be modelled as such. So for instance rather than duplicating the ID field on the User entity, you would have a property referencing a Customer; creating a User would require first creating a Customer (and thus assigning it an ID).

The User entity might then expose an interface of "get User ID", which happened to be implemented by returning an unmodified Customer ID. If in future it was decided that the one-to-one mapping of Users to Customers needed to change, that implementation could be changed, and code would already be clearly distinguishing the two sets of IDs.

Some other aggregates need to know about this new user, and a domain event seems not appropriate.

If User is not part of the domain layer, this statement is already false: the requirement "do this when a User is created" can't exist in a layer where "User" has no meaning. So either those actions also belong in the use case layer, or they can be re-written without reference to "user", e.g. "finalise user setup" causes a domain event of "open customer account".

I find a key mental exercise for separating layers is imagining what alternative implementations might look like at a layer. If User is truly outside of the domain layer, we should be able to imagine an application that doesn't have it, but still uses the domain layer correctly. So to take that last example, we could imagine a batch import causing the "open customer account" domain event without touching any user entities.

3
  • At the moment we are linking Customers to users because the id is the same. Do see that as potential issue? Commented Jul 6, 2020 at 17:12
  • @PedroRodrigues Previous comment has been expanded into the answer.
    – IMSoP
    Commented Jul 6, 2020 at 17:36
  • Thanks. This indeed provided a good insight into the problem. Our confusion, as far as I can tell atm, derives from using a user entity where in fact we need something else, something that that user entity has; rather than needing the entity itself. Appreciate the effort very much. Commented Jul 6, 2020 at 18:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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