Conclusion and Next Steps — Introducing DDD Incrementally
What you learned in this book
In this book, you learned about DDD and clean architecture for Laravel developers.
Organizing the key points
The essence of DDD
| Concept | Point | Effect |
|---|---|---|
| Value object | No identifier, immutable, compared by all attributes | Type safety, consolidated business rules |
| Entity | Has an identifier, has a lifecycle | Expresses business rules |
| Aggregate | A consistency boundary | The unit of a transaction |
| Repository | Abstracting persistence | Testability, separation of concerns |
The rules of the architecture
| Layer | What it knows | What it does not know |
|---|---|---|
| Controller | HTTP request/response | Business rules, DB structure |
| UseCase | How to use domain objects | The HTTP format, DB structure |
| Domain Entity | Business rules | The persistence method, the framework |
| Repository | The table structure, Eloquent | The HTTP format |
Transaction management
| Scenario | Transaction location | Reason |
|---|---|---|
| Updating only one aggregate | Repository | Guarantees the aggregate's consistency |
| Updating multiple aggregates | UseCase | Guarantees the consistency of multiple aggregates |
A roadmap for incremental adoption
The recommended approach when introducing DDD into an existing project:
Phase 1: Introducing value objects
Risk: low / Effect: high / Impact on existing code: small
This is the easiest phase to adopt and the one whose effect is easiest to see. The definitions of EmailAddress and UserName are the ones from Chapter 5 "Value Objects", so all you add here is the usage.
// Step 1: introduce the value object (EmailAddress goes under app/Domain/Shared/, UserName under app/Domain/User/; the namespace follows the directory)
$email = EmailAddress::fromString(' Taro@Example.jp ');
// fromString absorbs the surrounding whitespace and the letter case
$email->value(); // 'taro@example.jp'
// Step 2: start using it from new code (deal with existing code later)
// Use the value object in a new method
public function createUser(EmailAddress $email, UserName $name): User
{
// ...
}
// Step 3: gradually replace existing code
// Before
$user->email = $request->input('email');
// After
$user->email = EmailAddress::fromString($request->input('email'))->value();
The priority of adoption targets:
| Priority | Target | Reason |
|---|---|---|
| High | Identifier (ID) | A large gain in type safety |
| High | Amount (Money) | Consolidating calculation logic |
| Medium | Email address | Consolidating validation |
| Medium | Address | Bundling multiple fields |
Phase 2: Separating the service layer
Risk: medium / Effect: high / Impact on existing code: medium
Separate the Fat Controller into a UseCase.
// Step 1: prepare the directory structure
// app/Application/UseCase/User/CreateUserUseCase.php
// Step 2: extract the business logic from the existing Controller
// Before: Fat Controller
class UserController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([/* ... */]);
// Move this logic to the UseCase
$user = new User($validated);
$user->save();
if ($user->isNewCustomer()) {
Mail::to($user)->send(new WelcomeMail());
$this->pointService->grantWelcomePoints($user);
}
return response()->json($user);
}
}
// Step 3: create the UseCase
final class CreateUserUseCase
{
public function __construct(
private readonly Mailer $mailer,
private readonly PointService $pointService,
) {}
public function execute(CreateUserCommand $command): User
{
$user = new User([
'email' => $command->email,
'name' => $command->name,
]);
$user->save();
if ($user->isNewCustomer()) {
$this->mailer->to($user)->send(new WelcomeMail());
$this->pointService->grantWelcomePoints($user);
}
return $user;
}
}
// Step 4: make the Controller thin
class UserController extends Controller
{
public function __construct(
private readonly CreateUserUseCase $createUserUseCase
) {}
public function store(CreateUserRequest $request): JsonResponse
{
$user = $this->createUserUseCase->execute($request->toCommand());
return response()->json($user, 201);
}
}
Tips for migration:
- Build new features with the UseCase structure from the start
- Migrate existing features gradually at the timing of modifications
- Write tests before refactoring
Phase 3: Entities and repositories
Risk: medium / Effect: high / Impact on existing code: large
Separate Eloquent models and DDD entities.
// Step 1: create a domain entity (separate from Eloquent)
// app/Domain/User/User.php
namespace App\Domain\User;
use App\Domain\Shared\EmailAddress;
use App\Domain\User\Exception\InvalidUserStateException;
final class User
{
private function __construct(
private readonly UserId $id,
private EmailAddress $email,
private UserName $name,
private UserStatus $status,
) {}
public static function create(UserId $id, EmailAddress $email, UserName $name): self
{
return new self($id, $email, $name, UserStatus::ACTIVE);
}
// Factory method that restores from the DB (used by the repository)
public static function reconstruct(
UserId $id,
EmailAddress $email,
UserName $name,
UserStatus $status,
): self {
return new self($id, $email, $name, $status);
}
// Business logic in the entity
public function deactivate(): void
{
if ($this->status === UserStatus::DEACTIVATED) {
throw new InvalidUserStateException('Already deactivated');
}
$this->status = UserStatus::DEACTIVATED;
}
public function id(): UserId
{
return $this->id;
}
public function email(): EmailAddress
{
return $this->email;
}
public function name(): UserName
{
return $this->name;
}
public function status(): UserStatus
{
return $this->status;
}
}
UserId and UserName are defined in Chapter 5. UserStatus and UserModel fall outside the topic of this section (separating the entity from the repository), so prepare them in your own project.
The exception classes live in a different namespace, so they are separate files. If you omit namespace, they become global classes and are no longer descendants of the App\Domain\Shared\Exception\DomainException that the match in Chapter 16 catches.
// Exceptions for the User aggregate (the base class is DomainException from Chapter 16)
// app/Domain/User/Exception/InvalidUserStateException.php
namespace App\Domain\User\Exception;
use App\Domain\Shared\Exception\DomainException;
final class InvalidUserStateException extends DomainException
{
public function __construct(string $message)
{
parent::__construct(message: $message, errorCode: 'INVALID_USER_STATE');
}
}
// app/Domain/User/Exception/UserNotFoundException.php
namespace App\Domain\User\Exception;
use App\Domain\Shared\Exception\DomainException;
use App\Domain\User\UserId;
final class UserNotFoundException extends DomainException
{
public function __construct(UserId $userId)
{
parent::__construct(
message: "User ID {$userId->value()} was not found",
errorCode: 'USER_NOT_FOUND',
);
}
}
// Step 2: create the repository interface
// app/Domain/User/UserRepositoryInterface.php
namespace App\Domain\User;
use App\Domain\Shared\EmailAddress;
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function existsByEmail(EmailAddress $email): bool;
public function save(User $user): void;
public function nextIdentity(): UserId;
}
// Step 3: create the Eloquent implementation
// app/Infrastructure/Repository/EloquentUserRepository.php
namespace App\Infrastructure\Repository;
use App\Domain\Shared\EmailAddress;
use App\Domain\User\User;
use App\Domain\User\UserId;
use App\Domain\User\UserName;
use App\Domain\User\UserRepositoryInterface;
use App\Domain\User\UserStatus;
use App\Infrastructure\Eloquent\UserModel;
final class EloquentUserRepository implements UserRepositoryInterface
{
public function findById(UserId $id): ?User
{
$model = UserModel::find($id->value());
return $model ? $this->toEntity($model) : null;
}
public function existsByEmail(EmailAddress $email): bool
{
return UserModel::where('email', $email->value())->exists();
}
public function save(User $user): void
{
$model = UserModel::findOrNew($user->id()->value());
$model->id = $user->id()->value();
$model->fill([
'email' => $user->email()->value(),
'name' => $user->name()->value(),
'status' => $user->status()->value,
])->save();
}
public function nextIdentity(): UserId
{
return new UserId((int) UserModel::max('id') + 1);
}
private function toEntity(UserModel $model): User
{
return User::reconstruct(
new UserId($model->id),
EmailAddress::fromString($model->email),
UserName::fromString($model->name),
UserStatus::from($model->status),
);
}
}
// Step 4: wire it up in a ServiceProvider
// app/Providers/RepositoryServiceProvider.php
namespace App\Providers;
use App\Domain\User\UserRepositoryInterface;
use App\Infrastructure\Repository\EloquentUserRepository;
use Illuminate\Support\ServiceProvider;
final class RepositoryServiceProvider extends ServiceProvider
{
public array $bindings = [
UserRepositoryInterface::class => EloquentUserRepository::class,
];
}
// Step 5: use the repository from a UseCase
// app/Application/UseCase/User/DeactivateUserUseCase.php
namespace App\Application\UseCase\User;
use App\Domain\User\Exception\UserNotFoundException;
use App\Domain\User\UserId;
use App\Domain\User\UserRepositoryInterface;
final class DeactivateUserUseCase
{
public function __construct(
private readonly UserRepositoryInterface $userRepository
) {}
public function execute(UserId $userId): void
{
$user = $this->userRepository->findById($userId)
?? throw new UserNotFoundException($userId);
$user->deactivate();
$this->userRepository->save($user);
}
}
Phase 4: Introducing domain events (optional)
Risk: low / Effect: medium / Impact on existing code: small
Make the coordination between aggregates event-driven.
// Resolve the tight coupling inside the existing UseCase
// Before
final class ConfirmOrderUseCase
{
public function execute(ConfirmOrderCommand $command): void
{
$order = $this->orderRepository->findById(new OrderId($command->orderId));
$order->confirm();
$this->orderRepository->save($order);
// These processes are tightly coupled to the UseCase
$this->inventoryService->decrease($order);
$this->mailer->send(new OrderConfirmationMail($order));
$this->pointService->grantPoints($order);
}
}
// After: loosely coupled with an event-driven approach
final class ConfirmOrderUseCase
{
public function execute(ConfirmOrderCommand $command): void
{
$order = $this->orderRepository->findById(new OrderId($command->orderId));
$order->confirm(); // records an OrderConfirmed event internally
$this->orderRepository->save($order); // dispatches the event on save
}
}
// Each process becomes an independent listener
class DecreaseInventoryOnOrderConfirmed { /* ... */ }
class SendOrderConfirmationEmail { /* ... */ }
class GrantLoyaltyPointsOnOrderConfirmed { /* ... */ }
Tips for an incremental transition
| Practice | Description |
|---|---|
| Strangler pattern | Do not replace old code all at once; apply gradually starting from new features |
| Test-first | Write tests before refactoring to ensure safety |
| Small PRs | Avoid large changes; split into reviewable units |
| Parallel operation | Let old and new code coexist temporarily |
| Feature flags | Make switching to the new implementation controllable |
Frequently asked questions
Q: Should you use DDD in every project?
No. DDD is suited to domains with complex business rules.
| Project characteristics | Recommended approach |
|---|---|
| A simple, CRUD-centric app | Active Record (Eloquent as-is) |
| Moderate complexity | Apply DDD only to the parts that need it |
| Many complex business rules | Full-fledged DDD |
Q: Is it bad to use an Eloquent Model directly as a domain entity?
It is technically possible, but it has the following problems.
| Problem | Description |
|---|---|
| Scattered business rules | Tightly coupled to the table structure |
| Hard to test | Cannot test without a DB |
| Hard to guarantee invariants | Attributes can be assigned from anywhere, so an invalid state is hard to prevent |
Q: What do you do if an aggregate grows too large?
An aggregate growing too large is a sign that the aggregate's boundary is not appropriate.
Countermeasures:
- Consider whether you can tolerate eventual consistency (see the aggregates chapter)
- Split the aggregate (e.g. separate a Payment aggregate from the Order aggregate)
- Separate the read model (CQRS)
CQRS (Command Query Responsibility Segregation) is a pattern that handles the "write (Command)" and "read (Query)" of data with separate models. Even for a complex aggregate, by providing a read-only view model, you can achieve both performance and maintainability.
Key points for success
- Whole-team understanding: DDD is a team activity, not an individual technique
- Incremental adoption: do not change everything at once; start small
- Collaboration with domain experts: do not proceed with engineers alone
- Continuous refactoring: improve the model as your understanding deepens
- Avoid over-abstraction: keep it simple
Next steps
Now that you have finished this book, what action should you take next? Below is a concrete learning path.
Step 1: Start small (1 to 2 weeks)
Goal: introduce value objects into part of an existing project.
What to do:
□ Identify "frequently used types" from the existing code (EmailAddress, UserId, Money, etc.)
□ Implement one value object
□ Try using the value object in a new feature
□ Have a team member review it
Recommended resources:
- Value objects — review Chapter 5 of this book
- Chapter 2 of Naruse Masanobu's "Introduction to Domain-Driven Design"
Step 2: Separate the UseCase layer (1 to 2 months)
Goal: refactor one Fat Controller.
What to do:
□ Choose the most complex Controller
□ Extract the business logic into a UseCase class
□ Write tests (unit tests for the UseCase layer)
□ Pair-program with a colleague
Recommended resources:
Step 3: Grow the domain model (3 to 6 months)
Goal: complete one bounded context.
What to do:
□ Talk with a domain expert (a weekly meeting)
□ Organize the ubiquitous language (create a glossary)
□ Design entities and aggregates
□ Implement the repository pattern
□ Refactor continuously
Recommended resources:
- Entities — designing identifiers and lifecycles
- Aggregates — how to draw consistency boundaries
- The repository pattern — the chapter for "Implement the repository pattern"
- Parts 2 and 3 of Eric Evans's "Domain-Driven Design"
Step 4: Practice as a whole team (6 months onward)
Goal: make DDD your team's standard.
What to do:
□ Hold a team study group (monthly)
□ Share DDD perspectives in code reviews
□ Document design patterns (in a team wiki)
□ Share your knowledge in internal lightning talks / blog posts
Recommended resources:
- About this guide — pick the chapters you need from the full list
- Vaughn Vernon's "Implementing Domain-Driven Design"
- Do not try to do everything at once: DDD is something you introduce incrementally
- Learn from failure: your first design will always change. That is normal
- Build a community: learn together with teammates and the community
- Produce output: organizing what you learn in a blog or a lightning talk deepens your understanding
References
For those who want to learn more deeply about DDD, here are recommended books, online resources, and communities.
Must-read books
Books in Japanese
1. Naruse Masanobu, "Introduction to Domain-Driven Design: Learn the Basics of DDD from the Bottom Up" (Shoeisha, 2020)
The first book you should read after this one.
- Carefully explains foundational concepts such as value objects, entities, and repositories
- Rich, practical code examples in C#
- "Why you do it that way" is clearly explained
- Also covers how to move from lightweight DDD to a rich domain model
Why we recommend it: there is some overlap with this book (the Laravel DDD book), but Naruse's book is more systematic, and explanations from a different perspective deepen your understanding.
2. Eric Evans, "Domain-Driven Design: Tackling Complexity in the Heart of Software" (Japanese edition: Shoeisha, 2011)
The original source of DDD. Commonly called "the Evans book" or "the blue book."
- Lets you understand the philosophy and background of DDD
- The original source for concepts such as ubiquitous language and bounded contexts
- You can understand it more deeply after you have gained practical experience
Note: because it is difficult, we recommend first building a foundation with Naruse's book or this book before tackling it.
3. Vaughn Vernon, "Implementing Domain-Driven Design" (Japanese edition: Shoeisha, 2015)
Commonly called "the Vernon book" or "the red book." A practical version of the Evans book.
- More implementation-oriented, with rich, concrete examples
- Covers advanced topics such as aggregate design, event sourcing, and CQRS
- Case studies of applying DDD in large-scale systems
How we recommend reading it: after you have actually started a DDD project, refer to it as a reference when you face a specific challenge.
4. Koichiro Matsuoka, "Domain-Driven Design: Sample Code & FAQ" (Japanese)
- A Q&A-format companion that answers implementation questions
- Resolves specific questions in a Q&A format
- Rich sample code
Books in English (if you have the bandwidth)
5. "Learning Domain-Driven Design" by Vlad Khononov (O'Reilly, 2021)
- A new DDD primer published in 2021
- Introduces modern approaches and tools
- Practical ways to slice bounded contexts, EventStorming, and more
6. "Domain-Driven Design Distilled" by Vaughn Vernon (Addison-Wesley, 2016)
- The essence of the Vernon book
- Lets you grasp the whole picture of DDD in a short time (a few hours)
- Also ideal for introducing it to teammates
Online resources
Official documentation and technical sites
Laravel official documentation
- Essential for understanding the service container and service providers
- Detailed usage of the Eloquent ORM
- Explanations of features that combine with DDD, such as testing, events, and queues
Martin Fowler's Bliki https://martinfowler.com/bliki/
- Explanations of DDD's foundational concepts (Aggregate, Repository, Value Object, etc.)
- Explanations of event-driven architecture and the CQRS pattern
- Articles full of Martin Fowler's insights
Especially recommended articles:
DDD Community https://dddcommunity.org/
- A community site that has existed since the early days of DDD
- It has not been updated since 2014, but the early conference materials and the list of books and articles remain available
Japanese DDD-related resources
1. little hands' lab
A DDD-specialized blog by Koichiro Matsuoka (@little_hand_s). It has many practical, in-depth articles on aggregate design, value objects, implementation patterns, and more.
https://little-hands.hatenablog.com/
Recommended topics:
- Aggregate design and implementation
- How to introduce value objects
- "Domain-Driven Design: Modeling/Implementation Guide" (his book)
2. The DDD tag on Qiita / Zenn
Many practitioners share their knowledge here. In particular, "failure stories" and "refactoring experience reports" offer a lot to learn.
3. DDD Alliance
A community of DDD practitioners in Japan. They held workshops and study groups through 2018, and the records of those events remain on connpass.
- Official site: https://www.ddd-alliance.org/
- Event information (connpass): https://ddd-alliance.connpass.com/
4. Conference talks
Past conference materials (Speaker Deck, SlideShare, etc.):
- PHP Conference
- Laravel JP Conference
- Events hosted by DDD Alliance
Example search terms: "domain-driven design practice," "Laravel DDD," "aggregate design"
5. YouTube channels
- Recordings of PHP Conference
- Individual technical-explanation channels
Tools and frameworks
EventStorming online tools
EventStorming is a workshop technique in which domain experts and developers explore the domain model together.
PHPStan / Psalm
- Static analysis tools
- Improve type safety and check the consistency of the domain model
How to join the community
Learning DDD is more effective when you learn with peers in a community than on your own.
Online communities
1. X (formerly Twitter)
Follow people who share information about DDD:
- Naruse Masanobu (@nrslib)
X does not let you read posts while logged out, so you need an account to follow these.
2. GitHub
Star and follow open-source DDD implementation examples:
- Read the code of DDD practice projects
- Join discussions in Issues/Discussions
- Publish your own code as OSS
Offline events
1. Study groups and meetups
- PHP Conference: a large annual event (check the official site; the timing varies by year)
Event information: you can search on connpass or TECH PLAY.
2. Start an internal study group
How to hold a DDD study group at your company:
- Invite 2 to 3 interested colleagues
- Read one chapter of a book together each week
- Review actual code together
- Summarize what you learn in an internal wiki
Output activities
1. Write blog posts
Recommended platforms:
- Zenn: for engineers, Markdown authoring, and you can even publish technical books
- Qiita: a long-established technical-article sharing site
- note: when you want to reach a broader audience
Example article themes:
- "We introduced DDD into a Laravel project"
- "A code comparison before and after introducing value objects"
- "The story of struggling with aggregate boundary design"
2. Try giving a talk (a lightning talk)
Start with a 5- to 10-minute lightning talk (LT):
- An LT at an internal study group
- A talk at a meetup
- Applying for an LT slot at a conference
3. Contribute to OSS
- A PR to a DDD library
- A proposal to improve sample code
- Translating documentation
For continuous learning
Regular retrospectives
Monthly retrospective:
□ What DDD patterns did I introduce this month?
□ What went well?
□ What should I improve?
□ What is my goal for next month?
Sharpen your code-review perspective
A DDD-oriented code-review checklist:
- Are business rules consolidated in the domain layer?
- Are value objects used appropriately?
- Are the aggregate boundaries appropriate?
- Is the direction of dependency correct? (outer → inner)
Keep reading notes
When reading a technical book:
- Note the important concepts
- Think about how to use them in your own project
- Record the results of actually trying them
- Review them regularly to retain the knowledge
- Do not fear asking questions: the questions you think "is it okay to ask this?" are the valuable ones
- Share failure stories: failure stories teach more than success stories
- Contribute even in small ways: fixing a typo or improving documentation is a fine contribution
- Give & take: do not just receive information; also share your own experience
In closing
DDD is not "technology for technology's sake"; it is a means of delivering business value through better software.
By leveraging Laravel's flexibility and incorporating DDD concepts at the appropriate level for your project's maturity, you can build applications with excellent maintainability and expressiveness.
What matters is not mechanically applying DDD's technical patterns, but understanding the essence of the domain and expressing it in code.
Use the knowledge you gained in this book and please practice it in a real project. Start small at first, and we encourage you to gradually practice DDD across your whole team.
We hope that DDD makes your Laravel development even better.
Thank you for reading all the way to the end.
What to read next
- About this guide — the book's structure and the list of all 20 chapters, for finding a chapter to reread
- Chapter 19 "In Practice: Implementing the Order System" — the example that cuts across all 4 layers, for checking things hands-on
- Chapter 3 "Why DDD" — the chapter to come back to when deciding whether to adopt it