14 Nov 2023



Intermediate

Aggregates: In Domain-Driven Design (DDD), an aggregate is a group of closely related domain objects treated as a singular unit. These aggregates comprise both entities and value objects, seen as a tightly connected whole.

Aggregates are typically composed of a root object and a number of child objects. The root object is the only object that can be referenced directly from outside the aggregate. The child objects can only be accessed through the root object.

Analogy 1: Shopping Cart and Items

Imagine a shopping cart. A shopping cart can contain many different items, such as apples, oranges, and bananas. But the shopping cart itself is a single unit. We don't add items to the apples or the oranges. We add them to the shopping cart.

public class CartItem
{
    public Guid Id { get; private set; }
    public string ProductName { get; private set; }
    public decimal Price { get; private set; }
    public int Quantity { get; private set; }

    // Constructor and methods for the CartItem
    public CartItem(Guid id, string productName, decimal price, int quantity)
    {
        Id = id;
        ProductName = productName;
        Price = price;
        Quantity = quantity;
    }

    public decimal GetTotal()
    {
        return Price * Quantity;
    }
}

public class ShoppingCart
{
    public Guid Id { get; private set; }
    private List<CartItem> _cartItems = new List<CartItem>();

    // Constructor and methods for the ShoppingCart
    public ShoppingCart(Guid id)
    {
        Id = id;
    }

    public void AddItem(string productName, decimal price, int quantity)
    {
        // Perform validation and business logic before adding the item to the shopping cart
        var cartItem = new CartItem(Guid.NewGuid(), productName, price, quantity);
        _cartItems.Add(cartItem);
    }

    public void RemoveItem(CartItem cartItem)
    {
        // Perform validation and business logic before removing the item from the shopping cart
        _cartItems.Remove(cartItem);
    }

    public IEnumerable<CartItem> GetItems()
    {
        // Return the items in the shopping cart
        return _cartItems.AsReadOnly();
    }

    public decimal GetTotal()
    {
        // Calculate the total price of all items in the shopping cart
        return _cartItems.Sum(item => item.GetTotal());
    }
}

In this example:

  • ShoppingCart is the aggregate root.
  • CartItem is an entity within the aggregate.
  • ShoppingCart manages a list of CartItem entities.

This model represents a shopping cart that can contain various items. The ShoppingCart aggregate encapsulates the logic for managing items within the cart, and changes to the state of items (adding and removing) are controlled through the aggregate root (ShoppingCart). This aligns with the principles of DDD, where an aggregate represents a cluster of domain objects treated as a single unit.

Analogy 2: Library and Bookshelf:

Consider a library with various bookshelves. Each bookshelf can hold different books, but the bookshelf itself is a distinct unit. Just like in Domain-Driven Design (DDD), where an aggregate is a cluster of domain objects treated as a single unit, in the library analogy, the bookshelf serves as an aggregate, encapsulating and managing the books it contains.

public class Book
{
    public Guid Id { get; private set; }
    public string Title { get; private set; }
    public string Author { get; private set; }
    public bool IsAvailable { get; private set; } = true;

    // Constructor and methods for the Book
    public Book(Guid id, string title, string author)
    {
        Id = id;
        Title = title;
        Author = author;
    }

    public void BorrowBook()
    {
        if (!IsAvailable)
        {
            throw new InvalidOperationException("Book is not available for borrowing.");
        }

        IsAvailable = false;
    }

    public void ReturnBook()
    {
        if (IsAvailable)
        {
            throw new InvalidOperationException("Book is already available; cannot return again.");
        }

        IsAvailable = true;
    }
}

public class Bookshelf
{
    public Guid Id { get; private set; }
    public string ShelfName { get; private set; }
    private List<Book> _books = new List<Book>();

    // Constructor and methods for the Bookshelf
    public Bookshelf(Guid id, string shelfName)
    {
        Id = id;
        ShelfName = shelfName;
    }

    public void AddBook(string title, string author)
    {
        // Perform validation and business logic before adding the book to the bookshelf
        var book = new Book(Guid.NewGuid(), title, author);
        _books.Add(book);
    }

    public void RemoveBook(Book book)
    {
        // Perform validation and business logic before removing the book from the bookshelf
        _books.Remove(book);
    }

    public IEnumerable<Book> GetBooks()
    {
        // Return the books on the bookshelf
        return _books.AsReadOnly();
    }
}

In this example:

  • Bookshelf is the aggregate root.
  • Book is an entity within the aggregate.
  • Bookshelf manages a list of Book entities.

This represents a simple model where a bookshelf contains various books. The Bookshelf aggregate encapsulates the logic for managing books on a particular bookshelf, and changes to the state of books (adding and removing) are controlled through the aggregate root (Bookshelf). This aligns with the principles of DDD, where an aggregate represents a cluster of domain objects treated as a single unit.

Analogy 3: Social Media Profile and Posts:

Think of a social media profile as an aggregate. The profile represents an individual entity, and within it, you can find various posts, photos, and comments. Each post is associated with the profile, much like how items are associated with a shopping cart. The profile acts as a boundary, ensuring that interactions and relationships are managed within the context of that specific user, aligning with the concept of aggregates in DDD.

public class Post
{
    public Guid Id { get; private set; }
    public string Content { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public Guid UserId { get; private set; }

    // Constructor and methods for the Post
    public Post(Guid id, string content, DateTime createdAt, Guid userId)
    {
        Id = id;
        Content = content;
        CreatedAt = createdAt;
        UserId = userId;
    }
}

public class Photo
{
    public Guid Id { get; private set; }
    public string Url { get; private set; }
    public DateTime UploadedAt { get; private set; }
    public Guid UserId { get; private set; }

    // Constructor and methods for the Photo
    public Photo(Guid id, string url, DateTime uploadedAt, Guid userId)
    {
        Id = id;
        Url = url;
        UploadedAt = uploadedAt;
        UserId = userId;
    }
}

public class Comment
{
    public Guid Id { get; private set; }
    public string Content { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public Guid UserId { get; private set; }

    // Constructor and methods for the Comment
    public Comment(Guid id, string content, DateTime createdAt, Guid userId)
    {
        Id = id;
        Content = content;
        CreatedAt = createdAt;
        UserId = userId;
    }
}

public class UserProfile
{
    public Guid UserId { get; private set; }
    public string UserName { get; private set; }
    private List<Post> _posts = new List<Post>();
    private List<Photo> _photos = new List<Photo>();
    private List<Comment> _comments = new List<Comment>();

    // Constructor and methods for the UserProfile
    public UserProfile(Guid userId, string userName)
    {
        UserId = userId;
        UserName = userName;
    }

    public void AddPost(string content)
    {
        var post = new Post(Guid.NewGuid(), content, DateTime.UtcNow, UserId);
        _posts.Add(post);
    }

    public void AddPhoto(string url)
    {
        var photo = new Photo(Guid.NewGuid(), url, DateTime.UtcNow, UserId);
        _photos.Add(photo);
    }

    public void AddComment(string content)
    {
        var comment = new Comment(Guid.NewGuid(), content, DateTime.UtcNow, UserId);
        _comments.Add(comment);
    }

    public IEnumerable<Post> GetPosts()
    {
        return _posts.AsReadOnly();
    }

    public IEnumerable<Photo> GetPhotos()
    {
        return _photos.AsReadOnly();
    }

    public IEnumerable<Comment> GetComments()
    {
        return _comments.AsReadOnly();
    }
}

In this example:

  • UserProfile is the aggregate root.
  • Post, Photo, and Comment are entities within the aggregate.
  • UserProfile manages lists of Post, Photo, and Comment entities.

This example demonstrates a social media profile aggregate where various types of content (posts, photos, and comments) are associated with the user profile. The UserProfile aggregate encapsulates the logic for managing these entities, and changes to the state of posts, photos, and comments are controlled through the aggregate root (UserProfile). Above pseudo-code snippets illustrate the basic structure and relationships in each analogy. In a real-world scenario, you would likely have more sophisticated implementations and considerations.

Here are some simple examples of aggregates:

  • An order aggregate might include the order itself, as well as the order items, shipping address, and billing address.

  • A product aggregate might include the product itself, as well as its price, inventory level, and any other related data.

  • A customer aggregate might include the customer's name, address, contact information, and order history.