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 "DDD" in Most Laravel Codebases ](#the-problem-with-quotdddquot-in-most-laravel-codebases)
2. [  Value Objects: Enforce Invariants, Not Just Types ](#value-objects-enforce-invariants-not-just-types)
3. [  DTOs: Structured Input, Not Fancy Arrays ](#dtos-structured-input-not-fancy-arrays)
4. [  Actions: One Class, One Job ](#actions-one-class-one-job)
5. [  Testing the Stack Without a Browser ](#testing-the-stack-without-a-browser)
6. [  Key Takeaways ](#key-takeaways)

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

  #laravel   #ddd   #clean-architecture   #php  

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

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

       Table of contents

1. [  01   The Problem With "DDD" in Most Laravel Codebases  ](#the-problem-with-quotdddquot-in-most-laravel-codebases)
2. [  02   Value Objects: Enforce Invariants, Not Just Types  ](#value-objects-enforce-invariants-not-just-types)
3. [  03   DTOs: Structured Input, Not Fancy Arrays  ](#dtos-structured-input-not-fancy-arrays)
4. [  04   Actions: One Class, One Job  ](#actions-one-class-one-job)
5. [  05   Testing the Stack Without a Browser  ](#testing-the-stack-without-a-browser)
6. [  06   Key Takeaways  ](#key-takeaways)

 The Problem With "DDD" in Most Laravel Codebases
------------------------------------------------

Most teams reach for a DDD vocabulary — Actions, DTOs, Value Objects — and end up with three extra abstraction layers that do nothing except shuffle arrays between classes. The goal of this article is to show you what each concept is *actually for*, and how to implement each one with just enough code to earn its keep.

---

Value Objects: Enforce Invariants, Not Just Types
-------------------------------------------------

A Value Object is not a typed array. It is a small, immutable object whose equality is defined by its *value*, not its identity, and which enforces its own invariants at construction time.

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

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

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

```

Now `$order->total` is always a valid `Money` object — no validation scattered across controllers.

---

DTOs: Structured Input, Not Fancy Arrays
----------------------------------------

A DTO carries validated, typed data across a boundary. In Laravel, the natural boundary is the HTTP layer → domain layer. Use PHP 8 readonly properties and a static factory that reads from a `FormRequest`:

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

    public static function fromRequest(CreateOrderRequest $request): self
    {
        return new self(
            customerId: $request->integer('customer_id'),
            total: new Money(
                $request->integer('amount_in_cents'),
                $request->string('currency')->toString(),
            ),
            notes: $request->string('notes')->trim()->toString(),
        );
    }
}

```

No base class, no macro magic — just a plain PHP object that is trivially constructable in tests.

---

Actions: One Class, One Job
---------------------------

An Action encapsulates a single use-case. It is not a service class with ten methods. It is not a job. It is the orchestration layer between your DTO and your domain models.

```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,
            'notes'       => $data->notes,
        ]);

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

        return $order;
    }
}

```

Wire it through the service container and call it from your controller:

```php
class OrderController extends Controller
{
    public function store(CreateOrderRequest $request, CreateOrder $action): JsonResponse
    {
        $order = $action->execute(CreateOrderData::fromRequest($request));

        return OrderResource::make($order)->response()->setStatusCode(201);
    }
}

```

The controller has zero business logic. The action has zero HTTP knowledge. Both are independently testable.

---

Testing the Stack Without a Browser
-----------------------------------

```php
it('creates an order and dispatches OrderCreated', function () {
    Event::fake([OrderCreated::class]);

    $data = new CreateOrderData(
        customerId: 1,
        total: new Money(5000, 'USD'),
        notes: 'Rush delivery',
    );

    $order = app(CreateOrder::class)->execute($data);

    expect($order->total->amountInCents)->toBe(5000);
    Event::assertDispatched(OrderCreated::class);
});

```

No HTTP overhead, no form parsing — just domain logic under test.

---

Key Takeaways
-------------

- **Value Objects** enforce invariants at construction; pair them with Eloquent casts to keep models clean.
- **DTOs** are typed, immutable data carriers — static factory methods on `FormRequest` keep the boundary explicit.
- **Actions** are single-responsibility orchestrators; inject dependencies via the constructor, not the `execute` method.
- Avoid base classes and trait soup — each concept earns its keep through simplicity, not inheritance.
- The entire stack is unit-testable without booting HTTP or a browser.

 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-2&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-2) 

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

  3 questions  

     Q01  Should Actions be dispatched as jobs or called synchronously?        Actions should be synchronous orchestrators. If you need async execution, wrap the Action call inside a dedicated Job class rather than making the Action itself a job — this keeps the two concerns separate and the Action fully unit-testable. 

      Q02  Do I need a package like lorisleiva/laravel-actions for this pattern?        No. The pattern described here uses plain PHP classes resolved from Laravel's service container. Third-party action packages add convenience features but also coupling. Start with plain classes and reach for a package only when the ergonomics genuinely justify the dependency. 

      Q03  Where should Value Object validation live — in the VO constructor or in a FormRequest?        Both, for different reasons. The FormRequest validates user-facing input and returns friendly error messages. The Value Object constructor enforces domain invariants and throws exceptions — it is the last line of defence regardless of how the object was constructed. 

  Continue reading

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

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

 [ ![Laravel AI SDK: Tool-Calling Agents and Conversation Persistence](https://cdn.msaied.com/260/8c84f424e42da01993c9ba4b8eb19655.png) laravel ai agents 

### Laravel AI SDK: Tool-Calling Agents and Conversation Persistence

Build reliable tool-calling AI agents in Laravel using the Prism package. Learn how to wire tools, persist con...

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

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-ai-sdk-tool-calling-agents-and-conversation-persistence) [ ![Laravel Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration](https://cdn.msaied.com/259/e8ce445f021c2b26ebe4dd5da50014f8.png) livewire laravel alpine 

### Laravel Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration

Go beyond the docs: understand how Livewire v3 diffs the DOM with morph markers, intercept the lifecycle with...

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

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-livewire-v3-internals-morph-markers-js-hooks-and-alpine-integration) [ ![Laravel Package Development: Service Providers, Auto-Discovery, and Config Merging](https://cdn.msaied.com/258/673a80fa8e42ae375a4bba21bdcd92ea.png) laravel packages service-providers 

### Laravel Package Development: Service Providers, Auto-Discovery, and Config Merging

Build a production-ready Laravel package from scratch — covering service provider design, auto-discovery via c...

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

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-package-development-service-providers-auto-discovery-and-config-merging-1) 

   [  ![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)
