Pattern: Action-Domain-Responder (ADR)

By @ryan · 2021-10-17 23:03 (edited)

Pattern: Action-Domain-Responder (ADR)

Most web application frameworks use the Model-View-Controller (MVC) pattern. This pattern was first introduced in the 70s and has been able to stand the test of time.

Based on the introduction date, it's clear that it was never designed or architected for web development. The web wasn't really a thing until the late 80s / early 90s.

Despite this, it has been the defacto way to build a server-driven web application for years and no doubt will be for years to come.

Outside of the MVC world there are plenty of other design patterns, but they're often unused or rarely mentioned in the Laravel universe.

I'd like to show you how to use the Action-Domain-Responder (ADR) pattern in a Laravel application and how it differs from the traditional MVC pattern.

What is an Action?

When it comes to ADR, an Action class is comparable to a single-action controller.

Each Action should only handle a single action in your application.

Imagine you're building a "show" page for a User. In a typical Laravel application your route definition might look something like this:

Route::get('/users/{user}', [UsersController::class, 'show']);

You have a UsersController that has multiple methods, each handling a different route.

You could also have a dedicated ShowUserController (or similar).

If we were to convert this route into an Action, we would do something like this:

Route::get('/users/{user}', ShowUserAction::class);

Where do Action classes get defined?

A normal Laravel application would store Controller classes in app/Http/Controllers.

Since we're using ADR, we can group our business-logic more tightly and use a more domain-driven (the D in ADR) folder structure.

In this instance I'd create an app/Users folder. This Users folder is going to store all of the components related to the user-oriented parts of my application.

The Action class could then be created in a new file app/Users/Actions/ShowUserAction.php.

What does an Action need to have?

To use an Action class for a route, you need to define an __invoke method on the class.

namespace App\Users\Actions;

class ShowUserAction
{
	public function __invoke()
	{
		//
	}
}

When you hit the /users/{user} endpoint, this method will be invoked.

How does an Action generate a Response?

Now that we know what Action classes are, we can look at returning Response objects.

The letter R in ADR stands for Responders. As the name suggests, these classes are responsible for generating a Response object.

Where do Responder classes get stored?

Similar to Action classes, we can create a dedicated folder for Responder classes.

Let's go with app/Users/Responders.

For our /users/{user} route, we should probably create a new ShowUserResponder class too.

namespace App\Users\Responders;

class ShowUserResponder
{
	
}

What does a Responder need to have?

The naming conventions here are completely up to you, but I generally create a respond method to generate the response.

namespace App\Users\Responders;

use Illuminate\Http\Response;

class ShowUserResponder
{
	public function respond()
	{
		
	}
}

Other methods names I've seen include send, generate and even __invoke.

Now that we've created our Responder class, we can get an instance of ShowUserResponder injected inside of the ShowUserAction constructor.

namespace App\Users\Actions;

use App\Users\Responders\ShowUserResponder;

class ShowUserAction
{
	public function __construct(
		protected ShowUserResponder $responder
	) {}

	public function __invoke()
	{
		return $this->responder->respond();
	}
}

At the moment, the ShowUserResponder isn't actually returning a Response.

We're showing a User, so we can use route-model binding to retrieve a User instance inside of the ShowUserAction::__invoke method.

namespace App\Users\Actions;

use App\Users\Responders\ShowUserResponder;

class ShowUserAction
{
	public function __construct(
		protected ShowUserResponder $responder
	) {}

	public function __invoke(User $user)
	{
		return $this->responder->respond($user);
	}
}

We also want to pass the User object through to the ShowUserResponder::respond method so that we can generate a valid Response.

namespace App\Users\Responders;

use Illuminate\Http\Response;

class ShowUserResponder
{
	public function respond(User $user)
	{
		return view('users.show', [
			'user' => $user,
		]);
	}
}

Now that we have the User object, we can use Laravel's view helper to create a View object which Laravel will automagically convert into a valid Response object.

What else should a Responder do?

Apart from generating a valid Response (or Responsable object), the Responder should be responsible for modifying anything response related.

This could be adding / modifying headers or changing the response format (i.e. use JSON instead of HTML via content-negotiation).

So, MVC or ADR?

Here's the list of things I think ADR does better than MVC:

1. Domain separation

The ADR pattern was specifically designed with domain-driven design in mind. The letter D literally stands for domain.

I think the pattern lends itself to domain separation better than MVC.

You can choose to group based on target instead of component type. You'll no longer have 6 or 7 folders inside of app/Http/Controllers, but instead will have 6 or 7 folders with an Http/Controllers folder.

Depending on the size and scale of your application though, this level of abstraction can be overkill. You end up spending more time figuring out where things should go, instead of what the things should do.

2. Separation of concerns

One thing that's common in MVC is bloating your controllers with lots of business logic (I'm guilty of this sometimes).

With ADR, it's clear that your Action classes are only responsible for handling the business logic and that your Responder classes should handle the response generation.

If you find yourself using response() or view() inside of the Action, you're doing ADR wrong.

I think this pattern also encourages the use of middleware and actual request interceptors too.

You can avoid authorization checks inside of your Action by using middleware to run those checks instead. These could be reusable middleware handlers registered on the global middleware stack or route specific handlers.

Keeping these checks out of the Action will help you further separate your concerns.

  • By @devcircus · 2021-10-28 02:12

    Really love the structure and clarity that ADR brings to a Laravel app. I used it on many projects until I switched to Livewire. I'm actually trying to work out an ADR-like structure for Livewire. Really just trying to work out ANY kind of consistent structure with Livewire as my components tend to be radically different from each other.

    • By @ryan · 2021-10-28 08:03

      When it comes to Livewire, I tend to follow the same structure as my controllers in term of naming and location (when possible).

      Alongside that, I'll use consistent method names for things like save(), update(), submit() etc.

  • By @mwikala · 2021-10-26 20:12

    Really interesting read! I've noticed a rise in adoption of the ADR pattern in the Laravel community for quite a bit now, especially with starter templates like Jetstream introducing it to most people who weren't aware of it.

    I think it's amazing and hopefully it becomes a standard in the Lara community, I just think it'll take a while for people to notice it's value.