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 "Just Use a Service Class" ](#the-problem-with-quotjust-use-a-service-classquot)
2. [  Actions: Single-Responsibility Invokables ](#actions-single-responsibility-invokables)
3. [  DTOs: Typed Input Boundaries ](#dtos-typed-input-boundaries)
4. [  Value Objects: Enforce Invariants at Construction ](#value-objects-enforce-invariants-at-construction)
5. [  Keeping It Lean ](#keeping-it-lean)
6. [  Takeaways ](#takeaways)

  ![Domain-Driven Design in Laravel: Actions, DTOs, and Value Objects Without Bloat](https://cdn.msaied.com/191/b53b3c0f8105fa204b8f4d48b75ebaad.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   The Problem With "Just Use a Service Class"  ](#the-problem-with-quotjust-use-a-service-classquot)
2. [  02   Actions: Single-Responsibility Invokables  ](#actions-single-responsibility-invokables)
3. [  03   DTOs: Typed Input Boundaries  ](#dtos-typed-input-boundaries)
4. [  04   Value Objects: Enforce Invariants at Construction  ](#value-objects-enforce-invariants-at-construction)
5. [  05   Keeping It Lean  ](#keeping-it-lean)
6. [  06   Takeaways  ](#takeaways)

 The Problem With "Just Use a Service Class"
-------------------------------------------

Every Laravel codebase eventually grows a `Services/` directory that becomes a graveyard of 800-line classes. The instinct to reach for DDD is right — but most implementations either go too far (full hexagonal ports-and-adapters) or not far enough (renaming `UserService` to `UserAction` and calling it done).

This article focuses on three concrete building blocks — **Actions**, **DTOs**, and **Value Objects** — and how to wire them together without inventing a framework inside your framework.

---

Actions: Single-Responsibility Invokables
-----------------------------------------

An Action is an invokable class that encapsulates one use-case. It is not a controller, not a job, not a service. It is the verb of your domain.

```php
// app/Domain/Billing/Actions/ChargeSubscription.php
final class ChargeSubscription
{
    public function __construct(
        private readonly PaymentGateway $gateway,
        private readonly SubscriptionRepository $subscriptions,
    ) {}

    public function __invoke(ChargeSubscriptionData $data): Receipt
    {
        $subscription = $this->subscriptions->findOrFail($data->subscriptionId);

        $charge = $this->gateway->charge(
            amount: $data->amount,
            paymentMethod: $subscription->paymentMethod,
        );

        $subscription->recordCharge($charge);

        return Receipt::from($charge);
    }
}

```

Resolve it via the container so dependencies are injected automatically:

```php
// In a controller or Livewire component
app(ChargeSubscription::class)(
    new ChargeSubscriptionData(
        subscriptionId: $subscription->id,
        amount: Money::of(4900, 'USD'),
    )
);

```

The controller stays thin. The action is testable in isolation. No service locator smell.

---

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

A DTO (Data Transfer Object) replaces the raw `array` or `Request` object that bleeds into your domain. Use PHP 8.x readonly properties — no library required.

```php
final readonly class ChargeSubscriptionData
{
    public function __construct(
        public int $subscriptionId,
        public Money $amount,
    ) {}

    public static function fromRequest(ChargeRequest $request): self
    {
        return new self(
            subscriptionId: (int) $request->validated('subscription_id'),
            amount: Money::of(
                (int) $request->validated('amount_cents'),
                $request->validated('currency', 'USD'),
            ),
        );
    }
}

```

The `fromRequest` factory keeps HTTP concerns out of the domain. Your action never touches `$request->input()`. Swap HTTP for a CLI command or a queued job without changing the action.

---

Value Objects: Enforce Invariants at Construction
-------------------------------------------------

A Value Object is immutable, identified by its value, and self-validating. `Money` is the classic example, but `EmailAddress`, `Slug`, `Coordinates`, and `IpRange` are equally useful.

```php
final readonly class EmailAddress
{
    public string $value;

    public function __construct(string $value)
    {
        $normalized = mb_strtolower(trim($value));

        if (! filter_var($normalized, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidEmailAddress($normalized);
        }

        $this->value = $normalized;
    }

    public function domain(): string
    {
        return substr($this->value, strpos($this->value, '@') + 1);
    }

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

```

Pair it with a custom Eloquent cast so persistence is transparent:

```php
class EmailAddressCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes): EmailAddress
    {
        return new EmailAddress($value);
    }

    public function set($model, $key, $value, $attributes): string
    {
        return $value instanceof EmailAddress ? $value->value : $value;
    }
}

```

Now `$user->email` is always a valid `EmailAddress` — no validation scattered across controllers.

---

Keeping It Lean
---------------

The trap is adding layers for their own sake. A few rules that prevent bloat:

- **One action per use-case.** If two use-cases share 80% of logic, extract a private method or a shared service — don't merge the actions.
- **DTOs are dumb.** No business logic inside a DTO. If you're tempted, that logic belongs in an Action or Value Object.
- **Value Objects are small.** If a Value Object needs a database query to validate itself, it is not a Value Object — it is a domain service.
- **Skip the interface for every action.** Interfaces add value when you have multiple implementations or need to mock at the boundary. For most actions, the concrete class is the interface.

---

Takeaways
---------

- Actions are invokable, single-purpose, and container-resolved — keep controllers and Livewire components thin.
- DTOs create a typed boundary between HTTP/CLI and your domain; use `readonly` classes in PHP 8.1+.
- Value Objects enforce invariants at construction and pair cleanly with Eloquent custom casts.
- Resist adding abstraction until the pain is real — the goal is clarity, not pattern completeness.

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

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

  3 questions  

     Q01  Should every Laravel project use Actions, DTOs, and Value Objects?        No. A simple CRUD app with a handful of models does not need this structure. Introduce Actions when controllers grow beyond two or three responsibilities, DTOs when you find yourself passing raw arrays between layers, and Value Objects when the same validation logic appears in multiple places. 

      Q02  How do Actions differ from Laravel's built-in Jobs?        Jobs are designed for asynchronous, queued execution and carry serialization concerns. Actions are synchronous domain operations resolved from the container. You can dispatch an Action's logic from inside a Job, but they serve different purposes and should not be conflated. 

      Q03  Do I need a package like `lorisleiva/laravel-actions` to use this pattern?        No. A plain invokable class resolved via `app()` or constructor injection is sufficient. Third-party packages add conveniences like automatic route binding or job dispatching from the same class, which can be useful but also blur the single-responsibility principle. 

  Continue reading

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

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

 [ ![Laravel Eloquent Global Scopes: Pitfalls, Testing, and Composing Them Safely](https://cdn.msaied.com/211/8b9b19e7ecbf690b182ffbe6bffc9530.png) laravel eloquent testing 

### Laravel Eloquent Global Scopes: Pitfalls, Testing, and Composing Them Safely

Global scopes are powerful but easy to misuse. Learn how to write, test, and safely compose Eloquent global sc...

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

 16 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-eloquent-global-scopes-pitfalls-testing-and-composing-them-safely) [ ![Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations](https://cdn.msaied.com/210/b47272214946c6adcd02ddf74b7df816.png) laravel eloquent database 

### Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations

Beyond belongsTo and hasMany lies a set of underused Eloquent relation techniques. This guide covers custom re...

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

 16 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/eloquent-custom-relations-polymorphic-pivots-hasmanythrough-tricks-and-raw-join-relations) [ ![New in Laravel 12: Features, Helpers, and Upgrade Notes](https://cdn.msaied.com/209/c713447686bc1eb0a921b4027e4e4df8.png) laravel php upgrade 

### New in Laravel 12: Features, Helpers, and Upgrade Notes

Laravel 12 ships with a refined starter kit system, per-request context propagation, and several quality-of-li...

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

 16 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/new-in-laravel-12-features-helpers-and-upgrade-notes) 

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