Laravel DDD: Actions, DTOs &amp; Value Objects | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com) - [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [About](https://msaied.com#about)

           [  Contact](https://msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com)
- [ProjectsCase studies](https://msaied.com/projects)
- [ArticlesEngineering notes](https://msaied.com/articles)
- [CertificatesCredentials](https://msaied.com/certificates)
- [AboutHow I work](https://msaied.com#about)
- [ContactGet in touch](https://msaied.com#contact)

  [Start a conversation](https://msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com)
2. /
3. [Articles](https://msaied.com/articles)
4. /
5. Domain-Driven Design in Laravel: Actions, DTOs, and Value Objects Without Bloat

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

 Skip the ceremony. Learn how to apply DDD building blocks — Actions, DTOs, and Value Objects — in a Laravel codebase without drowning in boilerplate or over-engineering every feature.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com#person) Published 18 Oct 2026 · Updated 18 Oct 2026 · 4 min read

ShareCopy linkCopied

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

  On this page +1. [The Problem With "DDD in Laravel" Articles](#the-problem-with-quotddd-in-laravelquot-articles)
2. [Value Objects: Encode Rules, Not Just Data](#value-objects-encode-rules-not-just-data)
3. [DTOs: Typed Input Boundaries, Not Just Arrays](#dtos-typed-input-boundaries-not-just-arrays)
4. [Actions: Single-Responsibility Use Cases](#actions-single-responsibility-use-cases)
5. [Avoiding the Bloat Trap](#avoiding-the-bloat-trap)
6. [Takeaways](#takeaways)

 The Problem With "DDD in Laravel" Articles
------------------------------------------

Most DDD tutorials for Laravel either go full hexagonal-architecture-with-ports-and-adapters, or they slap a `app/Actions` folder on a standard Laravel app and call it done. Neither extreme is useful day-to-day.

This article shows the middle path: apply the three most valuable DDD building blocks — **Actions**, **DTOs**, and **Value Objects** — in a way that adds clarity without adding ceremony.

---

Value Objects: Encode Rules, Not Just Data
------------------------------------------

A Value Object is immutable, equality is based on its contents, and it enforces its own invariants. The classic mistake is making them anemic wrappers.

```php
// Bad: just a wrapper
final class Money
{
    public function __construct(public readonly int $amount) {}
}

// Good: enforces invariants and carries behaviour
final class Money
{
    private function __construct(
        public readonly int $cents,
        public readonly string $currency,
    ) {}

    public static function of(int $cents, string $currency): self
    {
        if ($cents < 0) {
            throw new \InvalidArgumentException('Amount cannot be negative.');
        }
        if (!in_array($currency, ['USD', 'EUR', 'GBP'], true)) {
            throw new \InvalidArgumentException("Unsupported currency: {$currency}");
        }
        return new self($cents, $currency);
    }

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

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

```

Pair this with a custom Eloquent cast so persistence is seamless:

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

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

```

---

DTOs: Typed Input Boundaries, Not Just Arrays
---------------------------------------------

DTOs replace the `$request->validated()` array that gets passed around and mutated. They are plain PHP objects — no base class required.

```php
final readonly class CreateOrderData
{
    public function __construct(
        public int    $customerId,
        public Money  $total,
        public string $shippingAddress,
        /** @var non-empty-list */
        public array  $lines,
    ) {}

    public static function fromRequest(Request $request): self
    {
        return new self(
            customerId:      (int) $request->validated('customer_id'),
            total:           Money::of(
                                 (int) $request->validated('total_cents'),
                                 $request->validated('currency'),
                             ),
            shippingAddress: $request->validated('shipping_address'),
            lines:           array_map(
                                 OrderLineData::fromArray(...),
                                 $request->validated('lines'),
                             ),
        );
    }
}

```

Using `readonly` (PHP 8.2+) removes the need for explicit `private` + getter boilerplate. The DTO is constructed once and never mutated — that's the contract.

---

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

An Action encapsulates one use case. It receives a DTO, coordinates domain objects and infrastructure, and returns a result. No controller logic, no model fat.

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

    public function execute(CreateOrderData $data): Order
    {
        $this->inventory->reserveLines($data->lines);

        $order = Order::create([
            'customer_id'      => $data->customerId,
            'amount_cents'     => $data->total->cents,
            'currency'         => $data->total->currency,
            'shipping_address' => $data->shippingAddress,
        ]);

        foreach ($data->lines as $line) {
            $order->lines()->create([
                'product_id' => $line->productId,
                'quantity'   => $line->quantity,
                'unit_cents' => $line->unitPrice->cents,
            ]);
        }

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

        return $order;
    }
}

```

Wire it through the service container and call it from both your HTTP controller and your Artisan command without duplication:

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

```

---

Avoiding the Bloat Trap
-----------------------

- **Don't create a DTO for every model.** Only introduce one when a use-case boundary genuinely needs typed input.
- **Don't create an Action for trivial CRUD.** A one-liner `Model::create($data)` does not need an Action wrapper.
- **Value Objects earn their keep when validation logic would otherwise scatter.** If you're checking the same currency list in three places, that's a Value Object waiting to be born.
- **Keep Actions free of HTTP concerns.** No `Request`, no `Response` inside an Action — those belong in the controller.

---

Takeaways
---------

- Value Objects enforce invariants at construction time and carry behaviour — they are not just typed arrays.
- `readonly` classes in PHP 8.2+ make DTOs concise without a base class or library.
- Actions are single-responsibility use cases that accept a DTO and return a domain result.
- The service container resolves Action dependencies automatically — no factory boilerplate.
- Apply these patterns selectively; complexity should follow business complexity, not the other way around.

- [laravel](https://msaied.com/articles?search=laravel)
- [ddd](https://msaied.com/articles?search=ddd)
- [clean-architecture](https://msaied.com/articles?search=clean-architecture)
- [php](https://msaied.com/articles?search=php)

 Frequently asked questions 
---------------------------

  Do I need a library like `spatie/laravel-data` to use DTOs in Laravel?No. PHP 8.2 readonly classes give you immutable, typed DTOs with zero dependencies. Libraries like spatie/laravel-data add validation casting and collection support, which is useful for complex APIs, but plain readonly classes are sufficient for most use cases.

   How do I test an Action in isolation without hitting the database?Inject repository interfaces rather than concrete Eloquent models, then swap them with in-memory fakes in your Pest tests. The Action itself becomes a pure function of its dependencies, making assertions straightforward without database setup.

   Should Value Objects implement Laravel's Castable interface?Yes, when they map to Eloquent model attributes. Implementing Castable lets the Value Object declare its own cast class, keeping the model definition clean: `protected $casts = \['total' =&gt; Money::class\]`.

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

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

[About](https://msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleFilament v3 Custom Field Plugins: Building Reusable Input Components from Scratch](https://msaied.com/articles/filament-v3-custom-field-plugins-building-reusable-input-components-from-scratch)  

   On this page
-------------

1. [The Problem With "DDD in Laravel" Articles](#the-problem-with-quotddd-in-laravelquot-articles)
2. [Value Objects: Encode Rules, Not Just Data](#value-objects-encode-rules-not-just-data)
3. [DTOs: Typed Input Boundaries, Not Just Arrays](#dtos-typed-input-boundaries-not-just-arrays)
4. [Actions: Single-Responsibility Use Cases](#actions-single-responsibility-use-cases)
5. [Avoiding the Bloat Trap](#avoiding-the-bloat-trap)
6. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/791/8bc85df3b88387f1d37d06a7c65b0de3.png)  · 4 min read### Filament v3 Custom Field Plugins: Building Reusable Input Components from Scratch

17 Oct 2026 ](https://msaied.com/articles/filament-v3-custom-field-plugins-building-reusable-input-components-from-scratch) [ ![](https://cdn.msaied.com/790/3b305e067cf21c59b93a26f96786fabd.png)  · 2 min read### PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection

17 Oct 2026 ](https://msaied.com/articles/postgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection-3) [ ![](https://cdn.msaied.com/789/40fed96a77e341e8fdd2b49f888d1d92.png)  · 4 min read### Event Sourcing in Laravel: Aggregates, Projectors, and Reactors Without a Framework

17 Oct 2026 ](https://msaied.com/articles/event-sourcing-in-laravel-aggregates-projectors-and-reactors-without-a-framework) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/sitemap.xml)
