Samuel Guedes

Domain-Driven Design: Building Software Around the Business

Summary

  1. What Is Domain-Driven Design?
  2. Why DDD Matters
  3. DDD vs Traditional CRUD Thinking
  4. Core Concepts of DDD
  5. Example: DDD in a Payment System
  6. Application Layer vs Domain Layer
  7. Benefits of DDD
  8. When Should You Use DDD?
  9. Common Mistakes When Learning DDD
  10. Final Thoughts

Software projects often start with technical decisions: which framework to use, how to structure folders, which database to choose, or how to expose APIs. These decisions are important, but they are not the heart of the system.

The real heart of most software is the business problem it solves.

This is where Domain-Driven Design, also known as DDD, becomes valuable. DDD is an approach to software engineering that helps developers design systems based on the business domain instead of only focusing on technical layers such as controllers, services, repositories, and databases.

In simple terms, Domain-Driven Design helps us write code that reflects how the business actually works.

What Is Domain-Driven Design?

Domain-Driven Design is a software design approach introduced by Eric Evans. Its main idea is that complex software should be built around a deep understanding of the domain.

The domain is the area of knowledge or business activity your software is dealing with.

For example:

  • In a banking system, the domain includes accounts, transfers, balances, limits, and transactions.
  • In an e-commerce system, the domain includes carts, orders, payments, products, and shipments.
  • In a travel platform, the domain includes bookings, tickets, hotels, customers, cancellations, and reservations.

DDD encourages developers and business experts to work closely together so that the software model matches the real business language, rules, and processes.

Why DDD Matters

Many applications start simple. At the beginning, a basic CRUD structure may be enough:

Create
Read
Update
Delete

But as the business grows, the logic becomes more complex.

For example, in a payment system, you may need rules like:

A payment starts as pending.
Only pending payments can be approved.
Only approved payments can be refunded.
Rejected payments cannot be approved again.
Refunded payments cannot be refunded twice.

If these rules are spread across controllers, services, database triggers, and background jobs, the system becomes hard to understand and maintain.

DDD helps solve this problem by keeping business rules in the domain layer, close to the concepts they belong to.

Instead of thinking only about database tables, DDD pushes us to think about business behavior.

A payment is not just a row in a database. It has states, rules, and actions.

DDD vs Traditional CRUD Thinking

Traditional CRUD thinking often leads to systems organized mainly around technical operations:

Create Payment
Update Payment
Delete Payment
Get Payment

DDD encourages us to think in terms of business operations:

Create Payment
Authorize Payment
Approve Payment
Reject Payment
Refund Payment
Cancel Payment

This difference is important.

CRUD describes what happens to data.

DDD describes what happens in the business.

A senior software engineer should not only ask, “How do I save this record?” They should also ask, “What does this operation mean for the business?”

Core Concepts of DDD

Domain

The domain is the business area your application is solving.

If you are building software for a financial company, your domain may include payments, accounts, customers, invoices, and transfers.

Understanding the domain is the first step. Before writing code, developers should understand the business language, rules, and processes.

Entity

An entity is an object that has identity.

For example, a payment can be an entity because each payment has a unique ID. Two payments may have the same amount and currency, but they are still different payments.

defmodule Payment do
  defstruct [
    :id,
    :amount,
    :status,
    :customer_id
  ]
end

The identity is what makes an entity unique.

Value Object

A value object is defined by its values, not by an identity.

A good example is money:

defmodule Money do
  defstruct [
    :amount,
    :currency
  ]
end

If you have two values of 10 USD, they can be considered equal because they represent the same value.

Value objects are useful because they make the domain more expressive and help protect business rules.

Instead of passing raw numbers everywhere, you can use a Money type to represent amount and currency together.

Aggregate

An aggregate is a group of related objects that must be changed together consistently.

For example, an order may contain order items, shipping information, and payment details.

Order
 ├── OrderItem
 ├── ShippingAddress
 └── PaymentInfo

The main object is called the aggregate root. In this case, Order is the aggregate root.

External code should modify the aggregate through the root. This protects the internal consistency of the business rules.

For example:

Order.add_item(order, product, quantity)
Order.confirm_payment(order)
Order.cancel(order)

Instead of allowing any part of the system to directly change order items, all changes go through the order itself.

Domain Service

Sometimes, business logic does not naturally belong to a single entity.

For example, payment authorization may involve a customer, a payment, fraud rules, and external limits. In this case, a domain service can be used.

defmodule PaymentAuthorizationService do
  def authorize(payment, customer) do
    # Business rules for payment authorization
  end
end

A domain service should contain business logic, not technical infrastructure logic.

Repository

A repository is responsible for storing and retrieving domain objects.

The domain should not need to know if the data comes from PostgreSQL, Redis, an external API, or another storage mechanism.

For example:

defmodule PaymentsRepository do
  def get_payment(id), do: ...
  def save(payment), do: ...
end

This creates a boundary between the business logic and the persistence layer.

In DDD, the database is important, but it should not control the design of the domain.

Bounded Context

A bounded context is one of the most important concepts in DDD.

It defines a clear boundary where a specific model is valid.

For example, in an e-commerce system, you may have different contexts:

Sales Context
- Cart
- Order
- Discount

Billing Context
- Invoice
- Payment
- Refund

Shipping Context
- Shipment
- TrackingCode
- DeliveryAddress

The same word can mean different things in different contexts.

For example, a customer in the sales context may be someone buying products. In the billing context, a customer may be someone responsible for invoices and tax information.

DDD helps us avoid creating one huge model that tries to represent everything in the system.

Instead, we create smaller models that make sense inside specific boundaries.

Example: DDD in a Payment System

Imagine we are building a payment system.

A simple technical structure might look like this:

PaymentController
PaymentService
PaymentRepository

This structure is not necessarily wrong, but it does not tell us much about the business.

A more domain-focused structure could look like this:

lib/my_app/payments/
  domain/
    payment.ex
    money.ex
    payment_status.ex

  application/
    create_payment.ex
    approve_payment.ex
    refund_payment.ex

  infrastructure/
    ecto_payment_repository.ex

  web/
    payment_controller.ex

In this structure, the domain layer contains the business concepts and rules.

For example:

defmodule MyApp.Payments.Payment do
  defstruct [
    :id,
    :amount,
    :status
  ]

  def new(amount) do
    %__MODULE__{
      id: Ecto.UUID.generate(),
      amount: amount,
      status: :pending
    }
  end

  def approve(%__MODULE__{status: :pending} = payment) do
    {:ok, %{payment | status: :approved}}
  end

  def approve(%__MODULE__{status: status}) do
    {:error, "Cannot approve payment with status #{status}"}
  end
end

The rule is clear:

Only pending payments can be approved.

This rule belongs to the domain. It should not be hidden inside a controller or duplicated across multiple services.

The controller should receive the request. The application layer should coordinate the use case. The domain should protect the business rules.

Application Layer vs Domain Layer

A common mistake when learning DDD is putting all logic into services.

DDD separates coordination logic from business logic.

The application layer coordinates a use case.

The domain layer contains business behavior and rules.

For example, approving a payment may involve:

Find payment
Validate business rule
Change payment status
Save payment
Publish event
Send notification

The application service can coordinate this flow, but the rule “only pending payments can be approved” should belong to the domain object.

A simple mental model is:

Controller = receives request
Application Service = coordinates use case
Domain = protects business rules
Repository = handles persistence
Infrastructure = database, APIs, queues, email, external services

Benefits of DDD

DDD brings several benefits to software projects.

First, it makes the code easier to understand because the code uses the same language as the business.

Second, it keeps business rules centralized. This reduces duplication and makes the system safer to change.

Third, it improves communication between developers, product managers, stakeholders, and domain experts.

Fourth, it helps create better boundaries in large systems. Instead of one big application with mixed responsibilities, the system can be separated into clear contexts.

Finally, DDD improves testability. Since business rules are isolated in the domain, they can be tested without depending heavily on databases, APIs, or web frameworks.

When Should You Use DDD?

DDD is powerful, but it is not necessary for every project.

It is useful when the business logic is complex.

Good examples include:

Banking systems
Payment platforms
Insurance systems
Marketplaces
Travel booking platforms
Healthcare systems
Logistics systems
ERP and CRM systems

DDD may be unnecessary for very simple applications, such as landing pages, small prototypes, or basic admin panels with little business logic.

The more complex the business rules are, the more value DDD can bring.

Common Mistakes When Learning DDD

One common mistake is thinking DDD is only about folder structure.

DDD is not just about creating folders named domain, application, and infrastructure. The real goal is to model the business correctly.

Another mistake is creating too many abstractions too early. DDD should simplify complex domains, not make simple systems more complicated.

A third mistake is allowing the database model to define the domain model completely. In DDD, the database supports the domain, but it should not be the only source of design.

Finally, many developers put all business rules in services. This often creates large service modules that become difficult to maintain. In many cases, behavior should be placed inside entities, value objects, or aggregates.

Final Thoughts

Domain-Driven Design is not just a technical pattern. It is a way of thinking about software.

It teaches us to focus on the business problem first and technical details second.

Instead of building systems around tables, endpoints, and frameworks, DDD helps us build systems around real business concepts, rules, and behaviors.

For developers who want to grow from mid-level to senior, learning DDD is extremely valuable. Senior engineers are expected to understand not only how to write code, but also how to design systems that represent the business clearly and can evolve over time.

A good first step is to choose a domain you know, such as payments, banking, e-commerce, or travel booking, and start modeling its rules.

Ask questions like:

What are the main business concepts?
What actions can happen?
What rules must always be protected?
What changes together?
Where are the boundaries?

When your code starts answering these questions clearly, you are no longer just writing CRUD operations.

You are designing software around the domain.