Domain-Driven Design has been around for over two decades, yet most teams still struggle to apply it effectively. After spending years implementing DDD across multiple enterprise projects, I want to share practical lessons that go beyond the typical "aggregate root" tutorials.
Start with the Problem, Not the Pattern
The single biggest mistake I see teams make is reaching for DDD patterns before understanding the domain. You do not need aggregates, value objects, and domain events on day one. You need conversations with domain experts.
In a recent logistics project, we spent three weeks doing Event Storming sessions before writing a single line of code. Those sessions revealed that "shipment" meant entirely different things to the warehouse team and the billing team, which directly influenced our bounded context boundaries.
Bounded Contexts Are Everything
Getting bounded contexts right is more important than getting your aggregate design perfect. A well-defined bounded context with mediocre internals will outperform a poorly bounded context with pristine aggregate design every time.
// Shipping context - Shipment is the aggregate root
public class Shipment
{
public ShipmentId Id { get; private set; }
public Address Origin { get; private set; }
public Address Destination { get; private set; }
public ShipmentStatus Status { get; private set; }
private readonly List<ShipmentEvent> _events = new();
public void Dispatch(CarrierId carrierId, DateTime estimatedDelivery)
{
if (Status != ShipmentStatus.Pending)
throw new InvalidOperationException("Only pending shipments can be dispatched.");
Status = ShipmentStatus.InTransit;
_events.Add(new ShipmentDispatched(Id, carrierId, estimatedDelivery));
}
}
// Billing context - same real-world shipment, different model
public class BillableShipment
{
public BillableShipmentId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public Money ShippingCost { get; private set; }
public BillingStatus BillingStatus { get; private set; }
}
Notice how the same real-world concept has two completely different representations. This is not duplication; this is proper separation of concerns.
Value Objects Deserve More Attention
Teams often skip value objects because they seem like unnecessary ceremony. In practice, they are one of the highest-value DDD patterns. A well-designed value object eliminates entire categories of bugs.
public record EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Email address cannot be empty.");
if (!value.Contains('@') || !value.Contains('.'))
throw new ArgumentException($"'{value}' is not a valid email address.");
Value = value.ToLowerInvariant().Trim();
}
public static implicit operator string(EmailAddress email) => email.Value;
}
Every time you pass around a string that represents an email, a URL, or a phone number, you are missing an opportunity to encode domain rules at the type level.
Domain Events for Cross-Context Communication
Domain events are the cleanest way to communicate between bounded contexts without creating tight coupling. We use MediatR for in-process events and a message broker for cross-service events.
public class OrderPlacedHandler : INotificationHandler<OrderPlaced>
{
private readonly IInventoryService _inventory;
private readonly INotificationService _notifications;
public async Task Handle(OrderPlaced notification, CancellationToken ct)
{
await _inventory.ReserveStockAsync(notification.Items, ct);
await _notifications.SendOrderConfirmationAsync(notification.CustomerId, ct);
}
}
Practical Takeaways
After years of applying DDD, my core advice is simple: invest heavily in understanding the domain before reaching for patterns. Use bounded contexts to manage complexity. Lean on value objects for type safety. And use domain events to keep your contexts decoupled.
DDD is not about the patterns. It is about building software that accurately models the business problem you are solving.
Comments (3)
I have a question: does this also apply to older versions?
Good question, I'd like to know too.
Exactly! I had the same thought.
Thanks for sharing — very helpful.
Could you elaborate on this topic in a follow-up post?
Leave a Comment