DDD modeling for a User Voice-like system I've taken on the challenge of modeling a simple User Voice-like system. High-level description:


It's a portal for some SaaS users;
They come and leave feature requests, suggestions, etc.;
They should be able to vote/unvote for any suggestions;
They can leave as many comments as they like on suggestions;
Comments may be removed by the owner, but may not be edited.


I've modeled the domain as follows, using a DDD approach. Please advise about mistakes, warnings, improvements, etc.

I've also applied the advices from these posts:


Don't create aggregate roots
Creating new aggregates in DDD
Link to an aggregate: reference or Id?


public abstract class Entity
{
    public Guid Id { get; protected set; } = Guid.NewGuid();
}

public class User : Entity // Aggregate Root
{
    public string Key =&gt; $"{Email}:{MarketplaceUrl}";

    public string Name { get; }

    public string Email { get; }

    public string MarketplaceName { get; }

    public Uri MarketplaceUrl { get; }

    internal User(string name, string email, string marketplaceName, Uri marketplaceUrl)
    {
        Name = name;
        Email = email;
        MarketplaceName = marketplaceName;
        MarketplaceUrl = marketplaceUrl;
    }

    public Suggestion MakeSuggestion(string text)
    {
        return new Suggestion(this, text);
    }
}

public class Suggestion : Entity // Aggregate Root
{
    public string Text { get; /* a suggestion cannot be altered */ }

    public User ByUser { get; }

    public DateTime SuggestedAt { get; }

    public ICollection&lt;Comment&gt; Comments { get; } = new List&lt;Comment&gt;();

    public ICollection&lt;Vote&gt; Votes { get; } = new List&lt;Vote&gt;();

    internal Suggestion(User byUser, string text)
    {
        ByUser = byUser;
        Text = text;
        SuggestedAt = DateTime.UtcNow;
    }

    public Comment AddComment(User byUser, string text)
    {
        var comment = new Comment(byUser, text);
        Comments.Add(comment);
        return comment;
    }

    public void RemoveComment(Comment comment, User userRemovingComment)
    {
        Comments.Remove(comment);
    }

    public void Unvote(User byUser)
    {
        var vote = Votes.SingleOrDefault(v =&gt; v.ByUser == byUser));

        if (vote != null)
            Votes.Remove(vote);
    }

    public Vote Vote(User byUser)
    {
        if (Votes.Any(v =&gt; v.ByUser == byUser))
            throw new CannotVoteTwiceOnSameSuggestionException();

        var vote = new Vote(byUser);
        Votes.Add(vote);
        return vote;
    }
}

public class Comment : Entity
{
    public string Text { get; /* a comment cannot be changed */ }

    public User ByUser { get; }

    public DateTime CommentedAt { get; }

    internal Comment(User byUser, string text)
    {
        ByUser = byUser;
        Text = text;
        CommentedAt = DateTime.UtcNow;
    }
}

public class Vote : Entity
{
    public User ByUser { get; }

    public DateTime VotedAt { get; }

    internal Vote(User byUser)
    {
        ByUser = byUser;
        VotedAt = DateTime.UtcNow;
    }
}

public interface IUserVoiceStore
{
    Task AddUserAsync(User user);

    Task AddSuggestionAsync(Suggestion suggestion);

    Task&lt;Suggestion&gt; GetSuggestionAsync(Guid id);

    Task&lt;User&gt; GetUserAsync(Guid id);

    // For when comments and votes are added/removed to/from a suggestion.
    Task UpdateSuggestionAsync(Suggestion suggestion);
}

public class UserVoiceService
{
    private readonly IUserVoiceStore store;

    public UserVoiceService(IUserVoiceStore store)
    {
        this.store = store;
    }

    public async Task&lt;User&gt; RegisterUserAsync(string name, string email, string marketplaceName, Uri marketplaceUrl)
    {
        var user = new User(name, email, marketplaceName, marketplaceUrl);
        await store.AddUserAsync(user);
        return user;
    }
}

public class CannotVoteTwiceOnSameSuggestionException : Exception { }

public class CannotRemoveCommentFromAnotherUserExcetion : Exception { }
The design looks solid, a few thoughts though:


I would split IUserVoiceStore into more granular UserRepository and SuggestionRepository. Also, UpdateSuggestionAsync() seems to indicate you can only update a suggestion and nothing else at a time, which can be limiting. It also IMO goes out of a repository's jurisdiction to flush a specific object to persistent storage. Using some kind of separate unit of work class where you can put multiple objects to be updated as part of a business transaction might be a better idea.
Not always feasible, but maybe change the type of link between Suggestion and User from a full reference to just an ID to avoid the temptation of manipulating 2 aggregate roots at the same time. (I don't agree with the article you linked to in that regard)
Keep an eye on suggestions with a large number of comments - depending on concurrent access, they can clog up your system and cause locking problems, especially if comments become more sophisticated, with images and so on.