Laravel DDD: Actions, DTOs &amp; Value Objects | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Domain-Driven Design in Laravel: Actions, DTOs, and Value Objects Without Bloat        On this page       1. [  The Problem With "Just Use DDD" ](#the-problem-with-quotjust-use-dddquot)
2. [  Value Objects: Replacing Primitives With Meaning ](#value-objects-replacing-primitives-with-meaning)
3. [  DTOs: Typed Input Boundaries ](#dtos-typed-input-boundaries)
4. [  Actions: Single-Responsibility Use Cases ](#actions-single-responsibility-use-cases)
5. [  Where People Over-Engineer ](#where-people-over-engineer)
6. [  Takeaways ](#takeaways)

  ![Domain-Driven Design in Laravel: Actions, DTOs, and Value Objects Without Bloat](https://cdn.msaied.com/651/39fb698517dd3f400bfae2ee03b70879.png)

  #laravel   #ddd   #clean-architecture   #php  

 Domain-Driven Design in Laravel: Actions, DTOs, and Value Objects Without Bloat 
=================================================================================

     10 Sep 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

1. [  01   The Problem With "Just Use DDD"  ](#the-problem-with-quotjust-use-dddquot)
2. [  02   Value Objects: Replacing Primitives With Meaning  ](#value-objects-replacing-primitives-with-meaning)
3. [  03   DTOs: Typed Input Boundaries  ](#dtos-typed-input-boundaries)
4. [  04   Actions: Single-Responsibility Use Cases  ](#actions-single-responsibility-use-cases)
5. [  05   Where People Over-Engineer  ](#where-people-over-engineer)
6. [  06   Takeaways  ](#takeaways)

 The Problem With "Just Use DDD"
-------------------------------

Domain-Driven Design gets a bad reputation in Laravel circles because most tutorials jump straight to full hexagonal architecture, repositories, aggregates, and domain events — before you've shipped a single feature. The result is a codebase that looks impressive in a conference talk and collapses under its own weight in production.

The good news: the three most valuable DDD building blocks — **Actions**, **DTOs**, and **Value Objects** — can be adopted incrementally, without a framework, and without rewriting your entire application.

---

Value Objects: Replacing Primitives With Meaning
------------------------------------------------

A Value Object wraps a primitive and enforces its own invariants. It has no identity — two `Money` objects with the same amount and currency are equal.

```php
final class Money
{
    public function __construct(
        public readonly int $amountInCents,
        public readonly string $currency,
    ) {
        if ($this->amountInCents < 0) {
            throw new \InvalidArgumentException('Amount cannot be negative.');
        }
        if (!in_array($this->currency, ['USD', 'EUR', 'GBP'], true)) {
            throw new \InvalidArgumentException("Unsupported currency: {$this->currency}");
        }
    }

    public function add(self $other): self
    {
        if ($this->currency !== $other->currency) {
            throw new \LogicException('Cannot add different currencies.');
        }
        return new self($this->amountInCents + $other->amountInCents, $this->currency);
    }

    public function equals(self $other): bool
    {
        return $this->amountInCents === $other->amountInCents
            && $this->currency === $other->currency;
    }
}

```

Pair this with a custom Eloquent cast so it survives the database round-trip:

```php
class MoneyCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes): Money
    {
        return new Money((int) $value, $attributes['currency']);
    }

    public function set($model, $key, $value, $attributes): array
    {
        return [
            $key => $value->amountInCents,
            'currency' => $value->currency,
        ];
    }
}

```

Now `$order->total` is always a valid `Money`, never a raw integer that silently accepts `-999`.

---

DTOs: Typed Input Boundaries
----------------------------

A DTO is a plain, immutable data carrier. Its job is to move validated data across a boundary — from a controller into a service or action — without leaking HTTP concerns.

```php
final readonly class CreateOrderData
{
    public function __construct(
        public int $customerId,
        public Money $total,
        public array $lineItems,
    ) {}

    public static function fromRequest(Request $request): self
    {
        $validated = $request->validate([
            'customer_id' => ['required', 'integer', 'exists:customers,id'],
            'total_cents' => ['required', 'integer', 'min:1'],
            'currency'    => ['required', 'string', 'in:USD,EUR,GBP'],
            'line_items'  => ['required', 'array', 'min:1'],
        ]);

        return new self(
            customerId: $validated['customer_id'],
            total: new Money($validated['total_cents'], $validated['currency']),
            lineItems: $validated['line_items'],
        );
    }
}

```

Using PHP 8.2+ `readonly` classes eliminates the need for a third-party DTO library entirely.

---

Actions: Single-Responsibility Use Cases
----------------------------------------

An Action encapsulates one use case. It is not a service with ten methods — it is one class, one public method.

```php
final class CreateOrder
{
    public function __construct(
        private readonly OrderRepository $orders,
        private readonly EventDispatcher $events,
    ) {}

    public function execute(CreateOrderData $data): Order
    {
        $order = Order::create([
            'customer_id' => $data->customerId,
            'total'       => $data->total,
            'status'      => OrderStatus::Pending,
        ]);

        foreach ($data->lineItems as $item) {
            $order->lineItems()->create($item);
        }

        $this->events->dispatch(new OrderCreated($order));

        return $order;
    }
}

```

Resolve it from the container in your controller:

```php
public function store(Request $request, CreateOrder $action): JsonResponse
{
    $order = $action->execute(CreateOrderData::fromRequest($request));
    return OrderResource::make($order)->response()->setStatusCode(201);
}

```

The controller is now a thin HTTP adapter. The action is testable in isolation with a mocked repository.

---

Where People Over-Engineer
--------------------------

- **Repositories for every model.** Use Eloquent directly until you have a real reason to swap the persistence layer.
- **Abstract base DTO classes.** PHP 8.2 `readonly` is enough.
- **Action interfaces.** Unless you're swapping implementations in tests, skip the interface.
- **Nested value objects three levels deep.** If you can't explain the invariant in one sentence, it's not a value object — it's a struct.

---

Takeaways
---------

- Value Objects enforce domain invariants at construction time, eliminating defensive checks scattered across your codebase.
- DTOs create a typed, HTTP-free boundary between your transport layer and your domain logic.
- Actions give each use case a home, making them independently testable and easy to locate.
- PHP 8.2+ `readonly` classes make DTOs zero-dependency.
- Adopt these three patterns incrementally — you don't need a full DDD stack to get the benefits.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fdomain-driven-design-in-laravel-actions-dtos-and-value-objects-without-bloat-3&text=Domain-Driven+Design+in+Laravel%3A+Actions%2C+DTOs%2C+and+Value+Objects+Without+Bloat) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fdomain-driven-design-in-laravel-actions-dtos-and-value-objects-without-bloat-3) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Do I need a package like `lorisleiva/laravel-actions` to use the Actions pattern?        No. A plain PHP class with a single `execute` method resolved from Laravel's service container is sufficient. Third-party action packages add conveniences like running actions as jobs or commands, which is useful but not required to start. 

      Q02  How do Value Objects interact with Eloquent's mass assignment and `$fillable`?        Value Objects are handled at the cast layer, not mass assignment. You store the primitive representation in the database column and cast it to a Value Object on read. The `$fillable` array should reference the underlying column name, not the Value Object class. 

      Q03  Should DTOs replace Form Requests in Laravel?        They complement each other. Keep Form Requests for HTTP validation rules and authorization. Use a DTO to carry the validated, typed data into your domain layer. The `fromRequest` factory method on the DTO is a clean bridge between the two. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Filament v4 Custom Field Plugins: Wrapping Third-Party JS Libraries Cleanly](https://cdn.msaied.com/650/513b93e06c50ba04f241beb1c3c16aa8.png) filament laravel alpine-js 

### Filament v4 Custom Field Plugins: Wrapping Third-Party JS Libraries Cleanly

Learn how to wrap any third-party JavaScript library into a reusable Filament v4 custom field plugin, with pro...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 10 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-custom-field-plugins-wrapping-third-party-js-libraries-cleanly) [ ![PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection](https://cdn.msaied.com/649/9ce340f6d71d0052cb3d0eaba1f08754.png) laravel postgresql performance 

### PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection

Window functions let you compute rankings, running totals, and detect gaps in sequences without subqueries or...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 9 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/postgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection-2) [ ![Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding Read Models](https://cdn.msaied.com/647/586a0f822614fed8091917a895ebc502.png) laravel event-sourcing ddd 

### Event Sourcing in Laravel: Aggregates, Projectors, and Rebuilding Read Models

A practical walkthrough of event sourcing in Laravel — defining aggregates, persisting domain events, building...

  ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said 

 9 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/event-sourcing-in-laravel-aggregates-projectors-and-rebuilding-read-models) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
