
Action-Domain-Responder
Introduction
Whenever I have been in any SOLID training, or any conversation among colleagues on the subject, a recurring question is always: ¿?How do you get a controller to follow a controller? the SRP?
Recall that the SRP is the principle of sole responsibility according to which, a class only has to have a single reason for change or in other words, the same thing: a single responsibility.
A controller, on the other hand, in an MVC architecture, is an intermediate layer between the view and the model, which is in charge of obtaining and transforming the model data into a valid response for the views.
In web frameworks such as Symfony or Laravel, controllers are the ones that define the routes that will be available to the application, acting as an entry point through the browser, obtaining and manipulating data from the model and passing it to the view.
The problem with this approach is that the controllers tend to become unmanageable for several reasons:
- The methods that resolve each path end up containing the use case itself, that is, the logic behind each action.
- Depending on how we decide to organize our routes, controllers tend to handle large numbers of routes, grouping several in the same class.
These two reasons, in turn, are the ones that break with the SRP: every route handled by a controller is a reason for change.
Goodbye MVC, welcome ADR
MVC
MVC is a software architecture pattern commonly used for the development of user interface applications. It boomed in the 1990s and early 2000s.
This pattern involves 3 actors:
- Model: It is the main component, it defines and manages the application data. It concentrates the business logic.
- View: It is the visual part of the application, the final representation of the data or actions performed for the user.
- Controller: It is the communication channel between the view and the model. The view can request information from the model through the controller, and the model will serve it back to the view through the controller.
MVC and current web frameworks
With the rise of web applications, this model was replicated for web applications as well, but it has never quite fit, as it is designed to represent small components within desktop applications.
If you have worked with Symfony you will know that it is not an MVC framework, as well as Laravel is not.
Symfony, for example, uses the Request/Response paradigm, whose flow can be seen in the following illustration:

Despite all this, the figure of the controller has hardly changed in all these paradigms, and one of the biggest problems is that the controller ends up becoming a catch-all, with a good part of the logic implemented in them and another part in the models.
Drivers in current web frameworks
This is what a controller of a Symfony or Laravel based project might look like:
<?php
namespace App\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Rsponse;
use Symfony\Component\Routing\Annotation\Rjute;
class BlogController extends AbstractController
{
/**
* @Route("/", defaults={"page": "1", "_format"="html"}, name="blog_index")
* @Method("GET")
*/
public function index(Request $request): Response
{
// Code to show all the posts
}
/**
* @Route("/posts/{slug}", name="blog_post")
* @Method("GET")
*/
public function postShow(Post $post): Response
{
// Code to show a single post
}
/**
* @Route("/comment/{postSlug}/new", name="comment_new")
* @Method("POST")
*/
public function commentNew(Request $request): Response
{
// Code to add a comment
}
/**
* @Route("/search", name="blog_search")
* @Method("GET")
*/
public function search(Request $request): Response
{
// Code to search
}
}As can be seen, this same class is managing 4 different actions:
- List all posts
- Show a specific post
- Add a comment
- Make a search
I'm not going to delve into the topic of annotations, this is just an example taken from a public Symfony repository, but it's always better to avoid them to avoid coupling. What I really want you to notice is that the controller has no less than 4 change reasons, and that's not good at all.
This way of organizing the routes in the controllers has become very widespread, to the point of finding controllers with more than a thousand lines, and that is much worse.
We recently saw how to extract the logic from our application and make it reusable by using the Command Pattern, and that's a first step in slimming down our controllers, but the problem with SRP is still there.
Action-Domain-Responder (ADR)
The ADR pattern is a refinement of the MVC pattern proposed by Paul M. Jones, which is best suited for web applications where MVC fails the most.
The pattern emulates HTTP (request-response) communication in a more efficient way. Its main actors are:
- Action: It is the equivalent of the MVC controller. It takes the input and uses it to interact with the request and the domain, to a single answer the output resulting.
- Domain: Equivalent to the MVC model. It interacts with the database and manipulates the information, it is again the one that contains the business logic.
- Answer: It represents the same as the MVC views. It is the one that shows the result of the action.
Although similar, they have their differences, and the most important one is in the pair Action/Controller.
While in MVC a controller contains several methods that correspond to different actions (or routes), in ADR an Action is a self-invoking class (it implements the method __invoke), it does not expose any more methods, so every new Action we want to model involves creating a new class.
Since version 2.7, Symfony gives us the option to define our controllers as services, which facilitates this implementation.
Let's look at the above example refactored to use ADR:
<?php
namespace App\Controller\Posts;
use Symfony\Component\HttpFoundation\Request;
class AddCommentController
{
public function __invoke(Request $request)
{
// Code to add a comment
}
}
services:
app.get_single:
class: AppBundle\Controller\Posts\GetSingleController
app.get_all:
class: AppBundle\Controller\Posts\GetAllController
app.add_comment:
class: AppBundle\Controller\Posts\AddCommentController
app.search:
class: AppBundle\Controller\Posts\SearchController<?php
namespace App\Controller\Posts;
use Symfony\Component\HttpFoundation\Request;
class GetAllController
{
public function __invoke(Request $request)
{
// Code to show all posts
}
}
<?php
namespace App\Controller\Posts;
use Symfony\Component\HttpFoundation\Request;
class GetSingleController
{
public function __invoke(Request $request)
{
// Code to show a single post
}
}
all_posts:
path: /posts
defaults: { _controller: app.posts.get_all }
single_post:
path: /post/{slug}
defaults: { _controller: app.posts.get_single }
add_comment:
path: /comment/{slug}/new
method: POST
defaults: { _controller: app.posts.add_comment }
search:
path: /search
defaults: { _controller: app.posts.search }<?php
namespace App\Controller\Posts;
use Symfony\Component\HttpFoundation\Request;
class SearchController
{
public function __invoke(Request $request)
{
// Code to add a comment
}
}The definition of controllers as services is optional as of version 3.3.
Conclusion
As can be seen, the separation brought about by this new paradigm is welcome, as our controllers have been reduced to mere request handlers, which handle a single request.
Advantages
- Greater stability. Now the controllers are services, we can inject the request we want and test all possible responses. By having only one method our tests will be very clean.
- Increased organization. The organization is now very structured, making it easy to locate the code that handles each route.
- Facility to enable/disable routes. If we ever want to disable a route, it's as simple as deleting in the routing the appropriate input.
- The controllers are now SRP compliant.
Find out more at our blog.

