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)    Domain-Driven Design in Laravel: Actions, DTOs, and Value Objects Without Bloat        On this page       1. [  Why DDD Primitives Without the Ceremony ](#why-ddd-primitives-without-the-ceremony)
2. [  Value Objects: Encapsulate Meaning, Not Just Data ](#value-objects-encapsulate-meaning-not-just-data)
3. [  DTOs: Typed Input Boundaries ](#dtos-typed-input-boundaries)
4. [  Actions: Single-Responsibility Use Cases ](#actions-single-responsibility-use-cases)
5. [  Avoiding Bloat: The Rules ](#avoiding-bloat-the-rules)
6. [  Takeaways ](#takeaways)

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

  #laravel   #ddd   #clean-architecture   #php  

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

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

       Table of contents

1. [  01   Why DDD Primitives Without the Ceremony  ](#why-ddd-primitives-without-the-ceremony)
2. [  02   Value Objects: Encapsulate Meaning, Not Just Data  ](#value-objects-encapsulate-meaning-not-just-data)
3. [  03   DTOs: Typed Input Boundaries  ](#dtos-typed-input-boundaries)
4. [  04   Actions: Single-Responsibility Use Cases  ](#actions-single-responsibility-use-cases)
5. [  05   Avoiding Bloat: The Rules  ](#avoiding-bloat-the-rules)
6. [  06   Takeaways  ](#takeaways)

 Why DDD Primitives Without the Ceremony
---------------------------------------

Domain-Driven Design carries a reputation for ceremony: aggregates, repositories, domain events, bounded contexts—all before you ship a single feature. In practice, most Laravel teams benefit from just three DDD primitives applied consistently: **Actions**, **DTOs**, and **Value Objects**. Used together they eliminate fat controllers, anemic models, and the "where does this logic live?" debate.

This article is opinionated. We skip the theory and go straight to patterns that work in production Laravel codebases.

---

Value Objects: Encapsulate Meaning, Not Just Data
-------------------------------------------------

A Value Object wraps a primitive and enforces its own invariants. It is immutable and compared by value, not identity.

```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 model stays clean:

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

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

```

Now `$order->total` returns a `Money` instance everywhere, not a raw integer.

---

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

A DTO (Data Transfer Object) carries validated, typed data across layer boundaries. It is not a model. It is not a form request. It is a plain PHP object that makes the shape of data explicit.

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

    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')),
            notes: $request->string('notes'),
            scheduledAt: new \DateTimeImmutable($request->string('scheduled_at')),
        );
    }
}

```

Using PHP 8.2+ `readonly` classes means zero boilerplate for immutability. The `fromRequest` factory keeps the HTTP layer out of your domain.

---

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

An Action is an invokable class that executes one use case. It accepts a DTO, coordinates domain objects, and returns a result. No static methods, no traits, no magic.

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

    public function execute(CreateOrderData $data): Order
    {
        $order = Order::create([
            'customer_id' => $data->customerId,
            'amount_in_cents' => $data->total->amountInCents,
            'currency' => $data->total->currency,
            'notes' => $data->notes,
            'scheduled_at' => $data->scheduledAt,
        ]);

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

        return $order;
    }
}

```

The controller becomes trivial:

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

```

Laravel's service container resolves `CreateOrderAction` automatically, including its dependencies.

---

Avoiding Bloat: The Rules
-------------------------

- **One Action per use case.** Resist the urge to add `$mode` flags or optional parameters.
- **DTOs are not validated inside themselves.** Validation belongs in Form Requests or dedicated validators before the DTO is constructed.
- **Value Objects throw on invalid input.** They are the last line of domain invariant enforcement.
- **Do not create a DTO for every array.** Only introduce one when the shape crosses a layer boundary or is reused in multiple Actions.
- **Actions are not jobs.** If you need async execution, dispatch a job that instantiates and calls the Action.

---

Takeaways
---------

- Value Objects enforce domain rules at the type level and pair cleanly with Eloquent casts.
- DTOs make layer boundaries explicit and eliminate primitive obsession in method signatures.
- Actions give each use case a single home, making testing and refactoring straightforward.
- PHP 8.2 `readonly` classes remove boilerplate from DTOs at zero runtime cost.
- Start with these three primitives before reaching for repositories, aggregates, or event sourcing.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fdomain-driven-design-in-laravel-actions-dtos-and-value-objects-without-bloat-1&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%2Fwww.msaied.com%2Farticles%2Fdomain-driven-design-in-laravel-actions-dtos-and-value-objects-without-bloat-1) 

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

  3 questions  

     Q01  Should Actions be dispatched as jobs or called directly?        Call Actions directly from controllers or other Actions. If you need async execution, create a dedicated Job class that instantiates the Action and calls it. This keeps the Action testable synchronously and the Job thin. 

      Q02  When should I introduce a DTO versus just passing an array?        Introduce a DTO when data crosses a layer boundary (HTTP → domain, job payload → domain) or when the same shape is consumed by more than one Action. For internal, single-use data within a method, a plain array is fine. 

      Q03  How do Value Objects work with Eloquent's dirty checking and mass assignment?        Use a custom Eloquent cast that serializes the Value Object back to its scalar columns in the `set` method. Eloquent's dirty checking compares the serialized scalars, so it works correctly. Exclude the cast attribute from `$fillable` and set it via the cast instead. 

  Continue reading

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

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

 [ ![Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware](https://cdn.msaied.com/216/9b6d240010b80483f072902dafcd216c.png) laravel middleware debugging 

### Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware

Telescope is powerful but heavy for production. Learn how to build a focused, low-overhead request inspector u...

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

 16 Jun 2026     1 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-telescope-alternatives-building-a-lightweight-request-inspector-with-middleware) [ ![RAG Pipelines in Laravel: Chunking, Embedding, and Retrieval with pgvector](https://cdn.msaied.com/215/e037e13535aa77822f879ee829ec3f68.png) laravel ai pgvector 

### RAG Pipelines in Laravel: Chunking, Embedding, and Retrieval with pgvector

Build a production-ready Retrieval-Augmented Generation pipeline in Laravel using pgvector, OpenAI embeddings,...

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

 16 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/rag-pipelines-in-laravel-chunking-embedding-and-retrieval-with-pgvector) [ ![Laravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI](https://cdn.msaied.com/214/0d4822fa8ee1765d0689e387dd849d92.png) laravel pest testing 

### Laravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI

Go beyond feature tests. Learn how to enforce architectural rules, catch logic gaps with mutation testing, and...

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

 16 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-pest-architecture-tests-mutation-testing-and-type-coverage-in-ci) 

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