DEV Community

Test
Test

Posted on • Edited on

Action-Domain-Responder VS. MVC: Laravel Implementation Example

Basically, Action-Domain-Responder (ADR) is simply an improved version of the classic Model-View-Controller (MVC) pattern, nothing more.

Here are the main advantages of ADR compared to MVC:

1. Better Domain Separation and Application Structure

  • ADR was originally designed with Domain-Driven Design (DDD) principles in mind, which allows better structuring of the application by domains rather than by component types (controllers, models, views). As a result, the code structure becomes more logical and focused on business goals rather than technical layers.

  • In MVC, you often end up with many folders full of controllers and mixed responsibilities, whereas ADR suggests grouping code by meaning and purpose, which makes maintenance and scaling easier.

2. Clear Separation of Responsibilities

In ADR, each component has a strictly defined role:

  • Action – handles business logic and interacts with the domain.
  • Domain – the business domain model.
  • Responder – builds the HTTP response (including headers and content).

In MVC, controllers often get bloated with business logic and responsibility for generating the response, which complicates maintenance and testing. In ADR, response generation is completely separated from business logic, improving readability and simplifying testing.

3. Closer Alignment with Web Architecture

  • ADR better reflects the real flow of web applications: a request goes to an action, the action interacts with the domain, then the response is formed. In MVC, the concept of a “view” is often mistakenly equated with a template, while in ADR the responder is responsible for the entire HTTP response.

  • ADR encourages the use of middleware and request interceptors, allowing authorization and other checks to be moved out of actions, further separating responsibilities.

4. Improved Maintainability and Scalability

Although ADR increases the number of classes (each action and responder is a separate class), this leads to a flatter and clearer hierarchy, making it easier to maintain and extend the application in the long term.

Example of ADR in Laravel

Action class:

<?php declare(strict_types=1);

namespace App\Account\User\Presentation\Action;

use App\Shared\Presentation\Controller;
use App\Shared\Domain\Bus\QueryBusInterface;
use App\Account\User\Application\Query\GetUsersPaginationQuery;
use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Route;
use Spatie\RouteAttributes\Attributes\WhereUuid;
use App\Account\User\Presentation\Responder\IndexResponder;
use App\Shared\Presentation\Response\ResourceResponse;

#[Prefix(prefix: 'v1')]
#[Middleware(middleware: 'auth:api')]
#[WhereNumber(param: 'perPage')]
final class IndexAction extends Controller
{
    private QueryBusInterface $queryBus;

    public function __construct(QueryBusInterface $queryBus)
    {
        $this->queryBus = $queryBus;
    }

    #[Route(methods: ['GET'], uri: '/users/{perPage?}')]
    public function __invoke(int $perPage = 11): ResourceResponse
    {
        $query = new GetUsersPaginationQuery(perPage: $perPage);

        return new IndexResponder()->respond(
            data: $this->queryBus->ask(query: $query)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Responder class:

<?php declare(strict_types=1);

namespace App\Account\User\Presentation\Responder;

use App\Account\User\Presentation\AccountResource;
use App\Shared\Presentation\Response\ResourceResponse;
use Illuminate\Http\Response;

final readonly class IndexResponder
{
    public function respond(mixed $data): ResourceResponse
    {
        if (!blank(value: $data)) {
            return new ResourceResponse(
                data: AccountResource::collection(resource: $data),
                status: Response::HTTP_OK
            );
        }

        return new ResourceResponse(
            data: ['message' => __('No Users Found.')],
            status: Response::HTTP_NOT_FOUND
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Structurally, it might look like this:

DDD Structura In Laravel

In conclusion

ADR is an evolution of MVC, adapted specifically for the web, providing better separation of concerns, cleaner architecture, and easier work with modern web applications.

Thus, Action-Domain-Responder is better than MVC because it:

  • Offers a more logical division of the application by domains rather than technical layers.
  • Clearly separates business logic handling from HTTP response formation.
  • Better matches the real flow of web interactions.
  • Encourages the use of middleware for code separation.
  • Provides simpler maintenance and scalability thanks to a clear class structure.

Top comments (8)

Collapse
 
xwero profile image
david duymelinck

I don't see the benefit of having a responder and an action? If they are inseparable why split the code?

If you do almost nothing in the controller and make it a single method class, why not add a callable in the router file?

// routes/api.php
Route::prefix('V1')
   ->middleware('auth:api')
   ->group(function() {
     Route::get('/'/users/{perPage?}', function (int $perPage = 11, QueryBusInterface $queryBus)
    {
        $query = new GetUsersPaginationQuery(perPage: $perPage, );

        return new IndexResponder()->respond(
            data: $queryBus->ask(query: $query)
        );
    }
   }
);
Enter fullscreen mode Exit fullscreen mode

Think about all the classes you don't have to write.

How does it scale better?

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
xwero profile image
david duymelinck

I agree that closures are an extreme way of doing things. I'm sorry that made you take a sidetrack.
I just wanted to make the point that having a lot of invokable classes is not inherently better than using controllers.

Controllers can have method dependency injection, which makes it possible that the methods can be tested in isolation. It is frowned upon to add all the dependencies using the constructor.
That is also what I wanted to show in the router example. But I agree you can't test that in isolation.

Controllers don't stop you from adding middleware. Laravel comes with quite a few added middlewares out-of-the-box.

Controllers don't stop you from having a domain driven design of your application. MVC for me is a part of the presentation layer.

Placing this kind of logic in route files in hexagonal, layered, or clean architecture is a mortal sin

If clean architecture leads to classes that could be functions, I am skeptical if that is what the code should be.

If you later need to add XML responses for the same endpoint, you’ll only modify the Responder without touching the Action logic

As I understand it a responder needs to be made aware how to behave by passing data.
So if you need a XMl response the action needs to pass that to the responder. Which means you do need to change the action.
With a controller that is only one file that changes.

Thread Thread
 
Sloan, the sloth mascot
Comment deleted
 
xwero profile image
david duymelinck

I appreciate the effort, but I don't think you are going to convince me it is a better pattern.

Collapse
 
meysam4n profile image
Meysam

I've seen different implementation of the ADR pattern, I have some questions about this way of the implementation:
1- Have you separated read and write repositories? Read Query classes, Write Command classes? kinda like CQRS pattern? is this your domain layer?
2- Don't you use Repository pattern?
3- Do you use DTOs for input values?
4- Why do you use Responders for each action? I think you can define some general Responders like SuccessfullResponder to handle many general endpoints or for one specific job like DeleteResponder to handle all delete action responses.
5- what is the use case of the QueryBusInterface interface? I mean is it a case to use multiple Queries in an action so it can handle all of them?