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.