14 Nov 2023
Aggregates in Domain-Driven Design have several key properties:
-
Consistency Boundary: When designing an aggregate, we should carefully consider which entities and value objects should be included within the aggregate boundary. All of the objects within the aggregate must be closely related and must work together to maintain a consistent state.
- Example 1 (E-commerce):
- Entities and Value Objects: Within an "Order" aggregate, the entities may include "OrderItem" and "ShippingAddress," and the value objects may include "Product" and "Price." These are closely related objects that collectively ensure the consistency of an order.
// Example 1: E-commerce class Order { List<OrderItem> orderItems; ShippingAddress shippingAddress; }- Example 2 (Social Networking):
- Entities and Value Objects: In a "User" aggregate, entities like "UserProfile" and "Friendship" and value objects like "EmailAddress" and "PhoneNumber" could be part of the aggregate, ensuring they work together to maintain a consistent state.
// Example 2: Social Networking class User { UserProfile userProfile; List<Friendship> friendships; } - Example 1 (E-commerce):
-
Aggregate Root: The aggregate root should be the most important entity within the aggregate and should be responsible for ensuring the consistency of the entire aggregate. For example, in an order aggregate, the order entity would likely be the aggregate root.
- Example 1 (E-commerce):
- Aggregate Root: In an "Order" aggregate, the "Order" entity serves as the aggregate root. It is responsible for managing the state of the entire order, including items, shipping details, and payment information.
// Example 1: E-commerce class Order { // Other properties... public void updateOrderStatus(Status newStatus) { // Implementation to update order status and ensure consistency } }- Example 2 (Social Networking):
- Aggregate Root: In a "BlogPost" aggregate, the "BlogPost" entity might act as the aggregate root, overseeing comments, likes, and tags associated with that particular blog post.
// Example 2: Social Networking class BlogPost { // Other properties... public void deletePost() { // Implementation to delete a blog post and associated data } } - Example 1 (E-commerce):
-
Transactionality: When performing operations on an aggregate, we should typically use a transaction to ensure that all of the changes within the aggregate are committed or rolled back as a single unit of work. This prevents the aggregate from being left in an inconsistent state.
- Example 1 (E-commerce):
- Transactionality: When updating the status of an "Order" (e.g., from 'pending' to 'shipped'), a transaction ensures that all related changes, such as updating inventory and processing payment, are committed or rolled back as a single unit.
// Example 1: E-commerce class OrderService { public void updateOrderStatus(Order order, Status newStatus) { // Transactional logic to update order status, handle inventory, and process payment // Changes will be committed or rolled back as a single unit of work } }- Example 2 (Social Networking):
- Transactionality: When a user deletes a "Comment" on a "Post," a transaction ensures that the deletion of the comment and any associated changes, like updating the post's comment count, are atomic.
// Example 2: Social Networking class CommentService { public void deleteComment(Post post, Comment comment) { // Transactional logic to delete comment and update post comment count // Changes will be committed or rolled back as a single unit of work } }- Example 3: In a financial application, a
BankAccountaggregate may include entities likeTransactionHistoryandAccountBalance. When transferring money between accounts, the entire operation, including updating the transaction history and account balances, is performed atomically.
public class BankAccount { private TransactionHistory transactionHistory; private AccountBalance accountBalance; // Transactional method public void transferMoney(BankAccount destinationAccount, BigDecimal amount) { // Logic for money transfer, updating transaction history and account balance } // Other methods... }- Example 4: In a hotel reservation system, a
Reservationaggregate may consist of entities likeReservationDetailsandPaymentStatus. Any changes to the reservation, including updating details and payment status, are committed or rolled back as a single transaction.
public class Reservation { private ReservationDetails reservationDetails; private PaymentStatus paymentStatus; // Transactional method public void updateReservationDetails(ReservationDetails newDetails) { // Logic to update reservation details, committing or rolling back as a transaction } // Other methods... } - Example 1 (E-commerce):
-
Encapsulation: We should encapsulate the aggregate's internal objects and only allow access to them through the aggregate root. This helps to prevent the aggregate's internal state from being corrupted.
- Example 1 (E-commerce):
- Encapsulation: The internal details of an "Invoice" object within an "Order" aggregate are hidden from external entities. Only the aggregate root, the "Order" entity, has direct access to and control over the "Invoice" object.
// Example 1: E-commerce class Order { // Other properties... private Invoice invoice; public void generateInvoice() { // Implementation to generate an invoice, encapsulating internal details } }- Example 2 (Social Networking):
- Encapsulation: Within a "User" aggregate, the details of a user's "Friendship" entity are encapsulated, and external entities can only interact with or modify the friendship status through the aggregate root, the "User" entity.
// Example 2: Social Networking class User { // Other properties... private Friendship friendship; public void addFriend(User friend) { // Implementation to add a friend, encapsulating friendship details } } - Example 1 (E-commerce):
-
Invariants and Business Rules: We should implement the aggregate's invariants and business rules within the aggregate root. This ensures that these rules are not violated by any operations performed within the aggregate.
- Example 1 (E-commerce):
- Invariants and Business Rules: In an "Account" aggregate, the aggregate root, representing a user account, enforces rules like "minimum account balance" and ensures that withdrawals and deposits maintain these invariants.
// Example 1: E-commerce class Account { BigDecimal balance; public void withdraw(BigDecimal amount) { // Implementation to enforce invariants like minimum balance } }- Example 2 (Social Networking):
- Invariants and Business Rules: In a "CalendarEvent" aggregate, the aggregate root, representing an event, enforces business rules like "start time must be before end time" to maintain consistency in scheduling.
// Example 2: Social Networking class CalendarEvent { LocalDateTime startTime; LocalDateTime endTime; public void setStartTime(LocalDateTime newStartTime) { // Implementation to enforce business rules like start time before end time } }- In a hotel booking system, a
RoomReservationaggregate may include entities likeReservationDetailsandGuestList. An invariant could be that a room cannot be double-booked for the same date and time, enforcing a business rule within the aggregate.
public class RoomReservation { private ReservationDetails reservationDetails; private GuestList guestList; // Method enforcing invariant public void bookRoom(Room room, Date startDate, Date endDate) { // Logic to check if the room is available for the specified dates // If available, book the room; otherwise, throw an exception } // Other methods... } - Example 1 (E-commerce):
-
Isolation: We should design our aggregates in such a way that they can operate relatively independently of each other. This allows us to scale our system more effectively and reduces contention in concurrent systems.
- Example 1 (E-commerce):
- Isolation: Each "ShoppingCart" aggregate operates independently. Users can add or remove items from their cart without affecting other users' carts, allowing for effective scalability.
// Example 1: E-commerce class ShoppingCart { // Other properties... public void addItem(Product product) { // Implementation for adding items to the shopping cart in isolation } }- Example 2 (Social Networking):
- Isolation: In a "Notification" aggregate, handling notifications for different users is isolated. Processing one user's notification doesn't interfere with the processing of notifications for other users.
// Example 2: Social Networking class NotificationProcessor { // Other properties... public void processNotification(User user, Notification notification) { // Implementation to process notifications in isolation for a specific user } } - Example 1 (E-commerce):
-
bounded: This means that the aggregate should represent a single, cohesive domain concept, and that its boundaries should be clear and well-defined. This helps to prevent the aggregate from becoming too complex and difficult to manage.
- Example 1 (E-commerce):
- Bounded: An "ProductCatalog" aggregate represents a cohesive concept within an e-commerce system. It encapsulates the logic and data related to managing product details and inventory.
// Example 1: E-commerce class ProductCatalog { // Implementation for managing product details and inventory }- Example 2 (Social Networking):
- Bounded: A "MessagingThread" aggregate represents a well-defined concept within a messaging system. It includes entities like "Message" and "Participant," forming a cohesive boundary around the messaging functionality.
// Example 2: Social Networking class MessagingThread { // Implementation for managing messages and participants within a messaging thread } - Example 1 (E-commerce):
-
immutable: This means that the state of the aggregate should not be changed directly from outside the aggregate. Instead, all changes to the aggregate should be made through the aggregate root. This helps to ensure that the aggregate's invariants and business rules are not violated.
- Example 1 (E-commerce):
- Immutable: In a "Price" value object within a "Product" aggregate, the price is not directly changed. Instead, a new "Price" object is created when adjustments are necessary, maintaining the immutability of past prices.
// Example 1: E-commerce class Price { BigDecimal amount; public Price adjustPrice(BigDecimal adjustment) { // Implementation to create a new Price object with adjusted amount } }- Example 2 (Social Networking):
- Immutable: The "UserProfile" entity within a "User" aggregate maintains immutability in certain aspects, like the user's original registration date. Changes to user profile details result in the creation of a new "UserProfile" object.
// Example 2: Social Networking class UserProfile { // Other properties... public UserProfile updateProfile(String newDetails) { // Implementation to create a new UserProfile object with updated details } } - Example 1 (E-commerce):
By following these principles, we can design and implement aggregates that help us to maintain a clean, consistent, and understandable domain model.