
Domain-Driven Design (DDD): Building Software Around Business Logic
Domain-Driven Design, usually known as DDD, is an approach to software design that puts the business domain at the center of the system. Instead of starting with database tables, routes, controllers, or framework folders, DDD starts with the real business problem: orders, payments, invoices, users, subscriptions, shipments, approvals, policies, and rules.
The main idea is simple but powerful: software should speak the language of the business. When the code uses the same concepts that business people use, the system becomes easier to understand, easier to change, and much safer to extend over time.
What Is Domain-Driven Design?
Domain-Driven Design is a software design approach focused on modeling complex business logic directly inside the application. It was introduced and popularized by Eric Evans, and it became especially important in systems where the business rules are more important than simple data storage.
DDD is not just a folder structure. It is not simply creating folders named Domain, Application, and Infrastructure. The real purpose of DDD is to make the code reflect the business reality as clearly as possible.
For example, in a simple CRUD system, you may have a table called orders and a controller method that updates the order status. But in a business-focused system, changing an order status may require many rules:
An order cannot be shipped before payment is confirmed.
An order cannot be cancelled after shipment.
A refund can only be created if the payment was captured.
A premium customer may have different cancellation rules.
An invoice must be generated after successful payment.
If these rules are scattered across controllers, jobs, observers, helpers, and database triggers, the system becomes hard to maintain. DDD tries to organize these rules inside meaningful business models.
The Core Problem DDD Solves
Many applications start clean but become difficult to maintain as business logic grows. At the beginning, developers usually place logic inside controllers, models, services, or even directly inside views. This works for small projects, but it becomes dangerous when the system grows.
A common problem is that the code becomes technically organized but business-wise confusing. You may have controllers, models, migrations, requests, resources, and jobs, but nobody can clearly answer where the real business rule lives.
For example:
public function cancel($id)
{
$order = Order::findOrFail($id);
if ($order->status === 'shipped') {
return response()->json(['message' => 'Cannot cancel shipped order'], 422);
}
if ($order->payment_status === 'captured') {
Refund::create([
'order_id' => $order->id,
'amount' => $order->total,
]);
}
$order->status = 'cancelled';
$order->save();
return response()->json(['message' => 'Order cancelled']);
}This code may work, but the business rule is trapped inside a controller. If the same cancellation logic is needed from an admin panel, API endpoint, command, job, or scheduled task, the rule may be duplicated. Later, when the business changes the cancellation policy, developers may update one place and forget another.
DDD solves this by moving the business rule into the domain model itself.
DDD Is About Business Language
One of the most important ideas in DDD is the Ubiquitous Language. This means developers and business people should use the same language when discussing the system.
If the business says “customer”, the code should not randomly use “user”, “client”, “account”, and “profile” for the same concept. If the business says “subscription renewal”, the code should not hide that behavior inside a generic method called updateStatus().
Good DDD code should make business behavior visible:
$order->cancel();
$invoice->markAsPaid();
$subscription->renew();
$shipment->dispatch();
$payment->capture();
$customer->upgradeToPremium();This type of code is easier to read because it describes business actions, not only technical operations.
Domain, Subdomain, and Business Context
The domain is the business area your software is built for. For an e-commerce application, the domain may include selling products online. For a hospital system, the domain may include appointments, patients, doctors, prescriptions, and billing. For a learning platform, the domain may include courses, lessons, students, certificates, and subscriptions.
Large domains are usually divided into smaller subdomains. Each subdomain represents a specific part of the business.
Example in an e-commerce system:
Catalog: products, categories, prices, availability.
Ordering: carts, orders, order items, order status.
Payment: payment authorization, capture, refund.
Shipping: shipment creation, tracking, delivery.
Customer Support: tickets, complaints, returns.
DDD helps you avoid mixing all these concepts into one giant system where every model knows too much about every other model.
Bounded Context: The Most Important Strategic Concept
A Bounded Context is a clear boundary where a specific business model is valid. The same word can mean different things in different parts of the business.
For example, the word Customer may have different meanings:
In the sales context, a customer is someone who buys products.
In the support context, a customer is someone who opens tickets.
In the billing context, a customer is someone who has invoices and payment methods.
Trying to create one universal Customer model for all contexts often creates a huge and confusing object. DDD recommends separating models based on context.
Instead of this:
App\Models\CustomerYou may have:
App\Domain\Sales\Models\Customer
App\Domain\Support\Models\Customer
App\Domain\Billing\Models\CustomerThis does not mean you always need separate database tables. It means the code model should be clear about which business context it belongs to.
Entity in DDD
An Entity is an object that has a unique identity and continues to exist even when its attributes change.
For example, an order is an entity. Its status may change from pending to paid to shipped, but it remains the same order because it has the same identity.
final class Order
{
private string $id;
private string $status;
private float $total;
public function __construct(string $id, float $total)
{
$this->id = $id;
$this->status = 'pending';
$this->total = $total;
}
public function pay(): void
{
if ($this->status !== 'pending') {
throw new DomainException('Only pending orders can be paid.');
}
$this->status = 'paid';
}
public function cancel(): void
{
if ($this->status === 'shipped') {
throw new DomainException('Shipped orders cannot be cancelled.');
}
$this->status = 'cancelled';
}
public function status(): string
{
return $this->status;
}
}The important point here is that the entity protects its own rules. Instead of allowing any part of the system to change the status directly, the entity exposes meaningful business methods such as pay() and cancel().
Value Object in DDD
A Value Object is an object that is defined by its value, not by identity. It does not need an ID. If two value objects have the same values, they are considered equal.
Examples of value objects:
Email address
Money
Address
Date range
Percentage
Coordinates
Instead of passing raw strings and numbers everywhere, value objects make the code safer and clearer.
final class Money
{
public function __construct(
private readonly int $amountInCents,
private readonly string $currency
) {
if ($amountInCents < 0) {
throw new InvalidArgumentException('Money amount cannot be negative.');
}
if ($currency === '') {
throw new InvalidArgumentException('Currency is required.');
}
}
public function amountInCents(): int
{
return $this->amountInCents;
}
public function currency(): string
{
return $this->currency;
}
public function add(Money $money): Money
{
if ($this->currency !== $money->currency) {
throw new DomainException('Cannot add money with different currencies.');
}
return new Money(
$this->amountInCents + $money->amountInCents,
$this->currency
);
}
}Without a value object, money may be represented as a float in one place, an integer in another place, and a string somewhere else. That leads to bugs. A value object gives the concept a clear shape.
Aggregate and Aggregate Root
An Aggregate is a group of related objects that must be kept consistent together. The Aggregate Root is the main object that controls access to the aggregate.
For example, an order may contain order items. The order is the aggregate root, and the items belong inside the order aggregate.
You should not usually modify order items directly from outside the order. The order should control how items are added, removed, or changed.
final class Order
{
private array $items = [];
public function addItem(string $productId, int $quantity, Money $price): void
{
if ($this->status !== 'pending') {
throw new DomainException('Cannot add items to a non-pending order.');
}
if ($quantity <= 0) {
throw new DomainException('Quantity must be greater than zero.');
}
$this->items[] = new OrderItem($productId, $quantity, $price);
}
public function total(): Money
{
$total = new Money(0, 'USD');
foreach ($this->items as $item) {
$total = $total->add($item->subtotal());
}
return $total;
}
}This protects business consistency. The order decides whether an item can be added. The order calculates its own total. The order controls the rules that belong to it.
Repository in DDD
A Repository is responsible for retrieving and saving domain objects. In DDD, repositories should feel like collections of domain objects, not like random database query helpers.
The domain layer should not care whether the data comes from MySQL, PostgreSQL, MongoDB, Redis, or an external API. That is infrastructure detail.
A repository interface may look like this:
interface OrderRepository
{
public function findById(string $id): ?Order;
public function save(Order $order): void;
}The implementation can use Laravel Eloquent, but the domain should not depend directly on Eloquent.
final class EloquentOrderRepository implements OrderRepository
{
public function findById(string $id): ?Order
{
$record = OrderModel::with('items')->find($id);
if ($record === null) {
return null;
}
return OrderMapper::toDomain($record);
}
public function save(Order $order): void
{
$record = OrderMapper::toEloquent($order);
$record->save();
}
}This separation makes the business logic easier to test and less dependent on the framework.
Domain Service
A Domain Service contains business logic that does not naturally belong to one entity or value object.
For example, calculating shipping cost may depend on order weight, customer location, delivery method, and external shipping zones. If the logic does not belong fully inside Order or Customer, it can live in a domain service.
final class ShippingCostCalculator
{
public function calculate(Order $order, Address $address): Money
{
if ($address->country() === 'TR') {
return new Money(5000, 'TRY');
}
if ($order->totalWeight() > 10000) {
return new Money(2500, 'USD');
}
return new Money(1500, 'USD');
}
}A domain service should still represent business logic. It should not become a technical service for sending emails, writing logs, or calling APIs. Those belong to the infrastructure layer.
Application Service
An Application Service coordinates a use case. It does not contain deep business rules. It receives input, loads domain objects, calls domain behavior, saves changes, and triggers external actions if needed.
Example:
final class CancelOrderService
{
public function __construct(
private readonly OrderRepository $orders
) {
}
public function handle(string $orderId): void
{
$order = $this->orders->findById($orderId);
if ($order === null) {
throw new RuntimeException('Order not found.');
}
$order->cancel();
$this->orders->save($order);
}
}The application service does not decide whether the order can be cancelled. That rule belongs inside the domain model. The service only coordinates the process.
Domain Events
A Domain Event represents something important that happened in the business.
Examples:
OrderWasPlaced
PaymentWasCaptured
InvoiceWasGenerated
SubscriptionWasRenewed
CustomerWasUpgraded
Domain events make the system more expressive. Instead of placing every side effect inside the same method, the domain can record that something happened. Other parts of the system can react to it.
final class OrderWasPlaced
{
public function __construct(
public readonly string $orderId,
public readonly string $customerId
) {
}
}Inside the aggregate:
final class Order
{
private array $events = [];
public function place(): void
{
if (empty($this->items)) {
throw new DomainException('Cannot place an empty order.');
}
$this->status = 'placed';
$this->events[] = new OrderWasPlaced(
$this->id,
$this->customerId
);
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}After saving the order, the application layer can publish these events. A listener may send an email, create an invoice, notify the warehouse, or update analytics.
DDD Layers Explained
A common DDD structure separates the system into layers. The exact folder names may change, but the idea remains the same.
Domain Layer: contains entities, value objects, aggregates, domain services, and domain events.
Application Layer: contains use cases, commands, handlers, and application services.
Infrastructure Layer: contains database implementations, external APIs, queues, mail, storage, and framework-specific details.
Interface Layer: contains controllers, API resources, requests, CLI commands, and UI adapters.
The most important rule is dependency direction. The domain should not depend on Laravel, Eloquent, HTTP requests, queues, or databases. The domain should contain pure business logic as much as possible.
Example Folder Structure in Laravel
Laravel does not force you to use DDD, but you can organize a Laravel application in a DDD-friendly way.
app/
├── Domain/
│ └── Orders/
│ ├── Entities/
│ │ ├── Order.php
│ │ └── OrderItem.php
│ ├── ValueObjects/
│ │ ├── Money.php
│ │ └── OrderStatus.php
│ ├── Events/
│ │ └── OrderWasPlaced.php
│ ├── Repositories/
│ │ └── OrderRepository.php
│ └── Services/
│ └── ShippingCostCalculator.php
│
├── Application/
│ └── Orders/
│ ├── Commands/
│ │ └── PlaceOrderCommand.php
│ └── Services/
│ └── PlaceOrderService.php
│
├── Infrastructure/
│ └── Persistence/
│ └── Eloquent/
│ ├── Models/
│ │ └── OrderModel.php
│ ├── Mappers/
│ │ └── OrderMapper.php
│ └── Repositories/
│ └── EloquentOrderRepository.php
│
└── Http/
└── Controllers/
└── OrderController.phpThis structure is useful when the project contains serious business rules. For very small CRUD projects, this may be too much. DDD should solve complexity, not create unnecessary ceremony.
Controller Example with DDD
In a DDD-style Laravel application, the controller should be thin. It should receive the request, validate input, call an application service, and return a response.
final class OrderController
{
public function __construct(
private readonly PlaceOrderService $placeOrderService
) {
}
public function store(PlaceOrderRequest $request): JsonResponse
{
$command = new PlaceOrderCommand(
customerId: $request->input('customer_id'),
items: $request->input('items')
);
$orderId = $this->placeOrderService->handle($command);
return response()->json([
'order_id' => $orderId,
'message' => 'Order placed successfully.',
], 201);
}
}The controller does not know how order totals are calculated. It does not know payment rules. It does not directly manipulate order status. It only delegates the use case.
Application Service Example
final class PlaceOrderService
{
public function __construct(
private readonly OrderRepository $orders,
private readonly ProductCatalog $products
) {
}
public function handle(PlaceOrderCommand $command): string
{
$order = Order::createForCustomer($command->customerId);
foreach ($command->items as $item) {
$product = $this->products->findById($item['product_id']);
if ($product === null) {
throw new RuntimeException('Product not found.');
}
$order->addItem(
productId: $product->id(),
quantity: $item['quantity'],
price: $product->price()
);
}
$order->place();
$this->orders->save($order);
return $order->id();
}
}The application service coordinates the use case. The order entity still owns the important business rules: whether an item can be added, whether the order can be placed, and how the total is calculated.
DDD vs Traditional CRUD
CRUD focuses on creating, reading, updating, and deleting records. This is enough for simple admin panels, small dashboards, and basic content management systems.
DDD is useful when the system has behavior, rules, workflows, and business decisions. In DDD, the question is not only “How do we update this row?” The better question is “What business action is happening, and what rules must be protected?”
| CRUD Thinking | DDD Thinking |
|---|---|
| Update order status | Cancel order, ship order, pay order |
| Save payment row | Capture payment or refund payment |
| Edit subscription | Renew, pause, upgrade, or cancel subscription |
| Change invoice field | Issue invoice, mark as paid, void invoice |
DDD encourages you to model business actions explicitly.
When Should You Use DDD?
DDD is useful when the business logic is complex enough that simple CRUD starts to fail.
You should consider DDD when:
The system has many business rules.
Different teams use different meanings for the same terms.
Business logic is duplicated across controllers, jobs, and services.
Changing one feature often breaks another feature.
The application has workflows such as orders, payments, approvals, invoices, subscriptions, or logistics.
You need strong testing around business behavior.
The project is expected to grow for years.
DDD is not necessary for every project. If you are building a simple blog, small landing page, or basic CRUD admin panel, traditional Laravel structure may be enough.
When DDD Can Be Too Much
DDD can become harmful when developers apply it mechanically without real business complexity. Creating too many layers, interfaces, factories, commands, handlers, and mappers can slow down development if the project does not need them.
Bad DDD often looks like this:
Every simple model has an interface without a reason.
Every operation has too many classes.
The domain layer contains no real business logic.
The folder structure looks advanced, but the code is still just CRUD.
Developers spend more time navigating files than solving business problems.
DDD should reduce business confusion. If it only adds technical complexity, it is being used incorrectly.
Common Mistakes in DDD
One common mistake is treating DDD as a folder architecture only. Moving files into Domain and Application folders does not automatically create good domain design. The important part is where the rules live and how clearly the code expresses the business.
Another mistake is making entities too empty. If your entity only has getters and setters, it is not really protecting business behavior.
// Weak model
$order->setStatus('cancelled');
// Better model
$order->cancel();The second version is better because it allows the order to check whether cancellation is allowed.
A third mistake is placing all logic inside services. This creates an anemic domain model. The entities become passive data containers, while services become huge procedural scripts.
A fourth mistake is mixing infrastructure with domain logic. For example, if an entity sends emails, writes logs, uses Laravel facades, or calls external APIs directly, the domain becomes coupled to technical details.
DDD and Testing
One major benefit of DDD is better testing. Because the domain logic is separated from controllers and databases, you can test business rules directly.
public function test_shipped_order_cannot_be_cancelled(): void
{
$order = Order::createForCustomer('customer-1');
$order->addItem('product-1', 1, new Money(1000, 'USD'));
$order->place();
$order->markAsShipped();
$this->expectException(DomainException::class);
$order->cancel();
}This test does not need HTTP, database migrations, factories, middleware, or API requests. It tests the business rule directly.
DDD in Real Projects
In real projects, DDD is often introduced gradually. You do not need to rewrite the entire system at once. A practical approach is to start with the most complex business area.
For example, in an e-commerce system, the product catalog may stay simple CRUD, while the ordering and payment modules use DDD because they contain more rules.
A realistic migration path may look like this:
Identify the most complex business workflow.
Write down the real business language used by the team.
Find entities, value objects, aggregates, and domain events.
Move business rules out of controllers.
Create application services for important use cases.
Introduce repositories only where persistence separation is useful.
Add tests for domain behavior.
Refactor gradually instead of rewriting everything.
DDD and Microservices
DDD is often mentioned with microservices, but they are not the same thing. DDD helps you understand business boundaries. Microservices are a deployment and architecture style.
A bounded context may become a microservice, but it does not have to. In many systems, it is better to start with a modular monolith. You can separate contexts inside one codebase first. Later, if there is a real need, some contexts can be extracted into services.
A good modular monolith with clear domain boundaries is usually better than a distributed system with unclear business boundaries.
Anti-Corruption Layer
An Anti-Corruption Layer protects your domain model from external systems or legacy code. It translates external data into concepts your domain understands.
For example, a payment provider may return statuses like:
AUTH_OKCAPTURED_FULLREVERSAL_DONE
Your domain may use clearer terms:
authorizedcapturedrefunded
The anti-corruption layer prevents external terminology from leaking into your business model.
final class PaymentStatusTranslator
{
public function translate(string $providerStatus): PaymentStatus
{
return match ($providerStatus) {
'AUTH_OK' => PaymentStatus::authorized(),
'CAPTURED_FULL' => PaymentStatus::captured(),
'REVERSAL_DONE' => PaymentStatus::refunded(),
default => throw new DomainException('Unknown payment status.'),
};
}
}Practical Checklist for Applying DDD
Before applying DDD, ask these questions:
What is the core business problem?
What words do business people use?
Which concepts have identity?
Which concepts are value objects?
Which objects must stay consistent together?
Where should each business rule live?
Which actions deserve explicit methods?
Which events are important to the business?
Which parts are domain logic and which parts are infrastructure?
Can the business rules be tested without HTTP and database setup?
Simple Rule to Remember
If the code says what the business means, you are moving in the right direction.
Instead of this:
$order->status = 'cancelled';
$order->save();Prefer this:
$order->cancel();
$orderRepository->save($order);The second version gives the domain model a chance to protect its own rules. That is the heart of DDD.
Conclusion
Domain-Driven Design is not about making software look more complicated. It is about making business complexity visible, organized, and protected. When used correctly, DDD helps developers build systems that match the real language, rules, and workflows of the business.
The most important lesson is that business logic should not be hidden inside controllers, database updates, or scattered service methods. It should live in expressive domain models that clearly describe what the system does.
DDD is especially powerful in applications with orders, payments, subscriptions, invoices, approvals, logistics, enterprise workflows, or any domain where rules matter more than simple data storage. Use it carefully, start with the most complex areas, and let the business model guide the structure of the code.

