Back to Blog
Domain-Driven Design in Laravel: Strategic and Tactical Patterns
What is Domain-Driven Design?
DDD is an approach to software development that prioritizes understanding and modeling the business domain. It's not about technology—it's about creating software that reflects how the business actually works.
Strategic Design: Bounded Contexts
Large systems should be divided into bounded contexts, each with its own model and language.
// E-commerce might have:
// - Catalog Context: Products, Categories, Pricing
// - Orders Context: Orders, LineItems, Checkout
// - Inventory Context: Stock, Warehouses, Reservations
// - Shipping Context: Shipments, Carriers, Tracking
// Each context has its own "Product" concept
namespace App\Catalog\Domain;
class Product { /* rich catalog data */ }
namespace App\Inventory\Domain;
class Product { /* stock-focused data */ }
Tactical Patterns
Entities
class Order
{
private OrderId $id;
private array $lineItems = [];
private OrderStatus $status;
public function addItem(Product $product, int $quantity): void
{
$this->guardAgainstModificationWhenPlaced();
$this->lineItems[] = new LineItem($product, $quantity);
$this->recalculateTotal();
}
private function guardAgainstModificationWhenPlaced(): void
{
if ($this->status !== OrderStatus::Draft) {
throw new OrderCannotBeModifiedException();
}
}
}
Value Objects
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency
) {
if ($amount < 0) {
throw new InvalidArgumentException('Amount cannot be negative');
}
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new CurrencyMismatchException();
}
return new self($this->amount + $other->amount, $this->currency);
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
Aggregates
// Order is an aggregate root
// LineItems are only accessed through Order
class Order // Aggregate Root
{
private array $lineItems; // Part of aggregate
public function getLineItems(): array
{
return $this->lineItems; // Return copies, not references
}
// All modifications go through the root
public function updateItemQuantity(LineItemId $id, int $quantity): void
{
$item = $this->findItem($id);
$item->updateQuantity($quantity);
$this->recalculateTotal();
}
}
Domain Events
class OrderPlaced implements DomainEvent
{
public function __construct(
public readonly OrderId $orderId,
public readonly CustomerId $customerId,
public readonly Money $total,
public readonly DateTimeImmutable $occurredAt
) {}
}
class Order
{
private array $domainEvents = [];
public function place(): void
{
$this->status = OrderStatus::Placed;
$this->domainEvents[] = new OrderPlaced(
$this->id,
$this->customerId,
$this->total,
new DateTimeImmutable()
);
}
public function pullDomainEvents(): array
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}
Repositories
interface OrderRepository
{
public function findById(OrderId $id): ?Order;
public function save(Order $order): void;
public function nextIdentity(): OrderId;
}
class EloquentOrderRepository implements OrderRepository
{
public function save(Order $order): void
{
DB::transaction(function () use ($order) {
$model = OrderModel::findOrNew($order->id()->value());
$model->fill($this->toArray($order));
$model->save();
// Dispatch domain events
foreach ($order->pullDomainEvents() as $event) {
event($event);
}
});
}
}
Conclusion
DDD provides tools for modeling complex domains. Use bounded contexts to manage complexity, value objects for immutable concepts, aggregates for consistency boundaries, and domain events for decoupled communication.
Related Articles
Need Help With Your Project?
I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.
Get In Touch