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

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

Menu
----

Close 

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

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

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/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 framework tax. Learn how to wire Actions, DTOs, and Value Objects into a Laravel codebase that stays lean, testable, and honest about its domain boundaries.

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

ShareCopy linkCopied

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

  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)

 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.

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

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

  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.

   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.

   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.

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

[Mohamed Said](https://www.msaied.com/public#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://www.msaied.com/public#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 articleModular Monolith in Laravel: Enforcing Bounded Contexts Without a Microservices Tax](https://www.msaied.com/public/articles/modular-monolith-in-laravel-enforcing-bounded-contexts-without-a-microservices-tax) [Next articleClean Architecture Testing with Pest: Actions, Fakes, and Boundary Contracts](https://www.msaied.com/public/articles/clean-architecture-testing-with-pest-actions-fakes-and-boundary-contracts)  

   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)

 ###  Have a technical challenge?

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

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

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

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

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

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

 [Discuss your project ↗](https://www.msaied.com/public#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://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/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://www.msaied.com/public/sitemap.xml)
