I try to follow the clean architecture principals and I was wondering what's the best approach for the following problem.
Let's say I have users that can follow each other and I want to save the specific time where the action has occurred. A user cannot follow itself nor a user that it has already followed.
// user aggregate
public class User : IAggregateRoot
{
public Guid Id { get; init; } = Guid.New();
public string Nickname { get; init; }
}
// follower aggregate
public class Follower : IAggregateRoot
{
private readonly List<Follow> _follows = new();
public Guid Id { get; init; } = Guid.New();
public Guid UserId { get; init; }
public void Follow(Guid userId)
{
if (userId == UserId)
throw new Exception(); // sample Exception for the sake of brevity
if (_follows.Any(f => f.UserId == userId))
throw new Exception(); // sample Exception for the sake of brevity
var follow = new Follow { UserId = userId, FollowedAt = Datetime.UtcNow };
_follows.Add(follow );
}
}
public class Follow
{
public Guid Id { get; init; } = Guid.New();
public Guid UserId { get; init; }
public Datetime FollowedAt { get; init; }
}
By splitting the classes in terms of business rules, how can I ensure there is a most one Follower
by user ?
How much should I keep my aggregates separated ? As far as I understood, if I wanted to allow my users to publish a few pictures, I should create a new Gallery
aggregate. Am I right ?
Should Follow
by a value object rather than an child entity as it does not contain any logic ?
By splitting the classes in terms of business rules, how can I ensure there is a most one Follower by user ?
Actually you don't need that in your domain layer.
Your domain aggregates should keep within the vicinity of your use case. Here, you want to handle the request for a new follow. You must access the whole Follower
and Follow
s collection to ensure the integrity of the domain and prevent having a user following multiple times the same user, breaking one of your domain requirements.
User
is a different aggregate that handles a different set of use cases and requirements, such as changing a user's nickname. In other terms, the User
aggregate is used for user management, whereas the Follow
aggregate is used for follow management.
These two aggregates can map to the same database table/column, including the identifier. This is called a polysemic domain model, you only load properties and joins that matter for your use cases, from a single normalized datamodel. The one-user-one-follower integrity is by design rather than code enforced.
How much should I keep my aggregates separated ? As far as I understood, if I wanted to allow my users to publish a few pictures, I should create a new Gallery aggregate. Am I right ?
It depends on the use cases and domain requirements. If the gallery is something that is independent from follower management and user management (besides reference to the user's id), then yes it is better to create a separate aggregate. This allows better separation of concerns by following a single responsibility principle.
You would add gallery in the existing user aggregate if, for instance, you had integrity rules that would tie user nickname with pictures.
Should Follow be a value object rather than an child entity as it does not contain any logic ?
It depends on whether you want to be able to address a specific Follow
object by id. I'd say you don't need that since you already are in the context of a given follower and one of the business rules makes the followed user id a good mean to select a given object.
Making them entities would make sense if there could have been multiple follows with the same users and the same date. You would have an easier time accessing a follow by id.
Here is how I would implement your code:
// user aggregate
public class User : IAggregateRoot
{
public Guid Id { get; init; } = Guid.New();
public string Nickname { get; private; }
// include user management use cases here
public void SetNickname(string value) { ... }
}
// follower aggregate
public class Follow
{
public Guid UserId { get; init; }
public DateTime FollowedAt { get; init; }
}
public class Follower : IAggregateRoot
{
private readonly List<Follow> follows;
public Guid UserId { get; init; }
public Follower(Guid userId, List<Follow> follows)
{
this.UserId = userId;
this.follows = follows ?? new List<Follow>();
}
public Follower() : this(Guid.NewGuid(), new List<Follow>())
{
}
public void Follow(Guid userId)
{
if (userId == this.UserId)
{
// business rule #1234
throw new BusinessException<FollowerErrors>(FollowerErrors.CannotSelfFollow);
}
if (this.follows.Any(follow => follow.UserId == userId))
{
// business rule #1235
throw new BusinessException<FollowerErrors>(FollowerErrors.AlreadyFollowed);
}
this.follows.Add(new Follow { UserId = userId, FollowedAt = DateTime.Now });
}
}
Here is the backing database model I would use: