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

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

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

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    DDD in Laravel: Actions, DTOs, and Value Objects Without the Bloat        On this page       1. [  DDD in Laravel Without the Ceremony ](#ddd-in-laravel-without-the-ceremony)
2. [  Value Objects: Encode Rules, Not Just Data ](#value-objects-encode-rules-not-just-data)
3. [  DTOs: Structured Input, Not Anemic Arrays ](#dtos-structured-input-not-anemic-arrays)
4. [  Actions: One Class, One Responsibility ](#actions-one-class-one-responsibility)
5. [  Where to Draw the Line ](#where-to-draw-the-line)
6. [  Key Takeaways ](#key-takeaways)

  ![DDD in Laravel: Actions, DTOs, and Value Objects Without the Bloat](https://cdn.msaied.com/317/04002c7e81deb9558c643f657ef94642.png)

  #laravel   #ddd   #clean-code   #architecture   #php  

 DDD in Laravel: Actions, DTOs, and Value Objects Without the Bloat 
====================================================================

     29 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   DDD in Laravel Without the Ceremony  ](#ddd-in-laravel-without-the-ceremony)
2. [  02   Value Objects: Encode Rules, Not Just Data  ](#value-objects-encode-rules-not-just-data)
3. [  03   DTOs: Structured Input, Not Anemic Arrays  ](#dtos-structured-input-not-anemic-arrays)
4. [  04   Actions: One Class, One Responsibility  ](#actions-one-class-one-responsibility)
5. [  05   Where to Draw the Line  ](#where-to-draw-the-line)
6. [  06   Key Takeaways  ](#key-takeaways)

 DDD in Laravel Without the Ceremony
-----------------------------------

Domain-driven design has a reputation for generating mountains of boilerplate. In practice, you can get most of the benefit — clear intent, testable business logic, and honest domain language — with three focused tools: **Actions**, **DTOs**, and **Value Objects**. The trick is knowing where each one earns its keep and where it just adds noise.

---

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

A Value Object wraps a primitive and enforces its own invariants. It is immutable, comparable by value, and self-validating. The moment you stop passing raw strings for things like money, email addresses, or percentages, an entire class of bugs disappears.

```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 and the database layer becomes transparent:

```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,
        ];
    }
}

```

---

### DTOs: Structured Input, Not Anemic Arrays

A DTO is a typed container that moves validated data across layer boundaries. It replaces the `$request->all()` array that leaks HTTP concerns into your domain.

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

    public static function fromRequest(Request $request): self
    {
        return new self(
            customerId: (int) $request->user()->id,
            total: new Money($request->integer('amount_cents'), $request->string('currency')),
            lineItems: $request->collect('items')->toArray(),
            couponCode: $request->string('coupon') ?: null,
        );
    }
}

```

Keep DTOs dumb. No business logic, no database calls. Their job is transport.

---

### Actions: One Class, One Responsibility

An Action encapsulates a single use-case. It accepts a DTO (or Value Objects directly), orchestrates domain logic, and returns a result. It is trivially testable because it has no HTTP or queue coupling.

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

    public function execute(CreateOrderData $data): Order
    {
        $discount = $data->couponCode
            ? $this->coupons->resolve($data->couponCode)
            : null;

        $order = Order::create([
            'customer_id' => $data->customerId,
            'total'       => $discount
                ? $data->total->subtract($discount->amount())
                : $data->total,
            'line_items'  => $data->lineItems,
        ]);

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

        return $order;
    }
}

```

Wire it in a controller with a single line:

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

```

---

### Where to Draw the Line

Not every field needs a Value Object. A user's `first_name` is fine as a string. Reserve Value Objects for concepts with **rules** — money, coordinates, email addresses, percentages, identifiers with a specific format.

Similarly, not every operation needs an Action class. A simple `User::find()` in a controller is not a sin. Reach for an Action when the logic involves multiple steps, side effects, or needs to be reused across HTTP, CLI, and queue contexts.

---

### Key Takeaways

- **Value Objects** enforce invariants at construction time and eliminate primitive obsession.
- **DTOs** decouple HTTP input from domain logic; keep them readonly and free of behaviour.
- **Actions** are single-responsibility orchestrators — easy to unit-test, easy to reuse.
- Combine all three with Laravel's custom casts and form requests for a clean, layered flow.
- Resist the urge to create abstractions for their own sake; every layer must justify its existence.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fddd-in-laravel-actions-dtos-and-value-objects-without-the-bloat&text=DDD+in+Laravel%3A+Actions%2C+DTOs%2C+and+Value+Objects+Without+the+Bloat) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fddd-in-laravel-actions-dtos-and-value-objects-without-the-bloat) 

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

  3 questions  

     Q01  Should Actions be resolved from the service container or instantiated manually?        Resolve them from the container. Laravel's automatic constructor injection means your Action receives its dependencies without any manual wiring, and you can swap implementations in tests by binding fakes in the container. 

      Q02  How do Value Objects interact with Eloquent's JSON columns?        Implement a custom cast that serialises the Value Object to a JSON-compatible array in `set()` and reconstructs it in `get()`. For single-column values like Money split across two columns, return an array from `set()` and declare `castUsing` with the extra column names. 

      Q03  Is a DTO different from a Laravel Data object (Spatie)?        Spatie's laravel-data package adds validation, transformation, and lazy loading on top of the DTO concept. It is a good fit for larger projects. For smaller domains, a plain readonly class with a static factory method is lighter and has zero extra dependencies. 

  Continue reading

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

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

 [ ![Filament v5 Preview: Schema Unification, Performance Shifts, and How to Prepare](https://cdn.msaied.com/340/1a05ca68637b898b676efb66f22e627f.png) filament laravel php 

### Filament v5 Preview: Schema Unification, Performance Shifts, and How to Prepare

Filament v5 is reshaping how panels, forms, and tables are composed. This deep-dive covers the confirmed archi...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 1 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/filament-v5-preview-schema-unification-performance-shifts-and-how-to-prepare) [ ![Laravel 13: New Features, Helpers, and Practical Upgrade Notes](https://cdn.msaied.com/339/58c4fa6fe9b6d25a2dac17c621b6f4c6.png) laravel laravel-13 upgrade 

### Laravel 13: New Features, Helpers, and Practical Upgrade Notes

Laravel 13 ships with async-first defaults, a leaner bootstrapping layer, and several quality-of-life helpers....

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 1 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-13-new-features-helpers-and-practical-upgrade-notes) [ ![Laravel 12: Structured Route Files, Slim Skeletons, and the New Application Bootstrapping](https://cdn.msaied.com/337/05b39d16d0f88a5fb94d0cf74049b88b.png) laravel laravel-12 upgrade 

### Laravel 12: Structured Route Files, Slim Skeletons, and the New Application Bootstrapping

Laravel 12 ships with a leaner skeleton, first-class route file organisation, and a revised application bootst...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 1 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-12-structured-route-files-slim-skeletons-and-the-new-application-bootstrapping) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.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://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.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)
