The Action Pattern for Business Logic in Laravel

Every Laravel app I build uses the action pattern. Not because it's trendy, but because it solves a real problem: business logic that's easy to find, easy to test, and easy to reuse.

What's an Action?

An action is a class that does one thing. It's a verb:

  • CreateOrder
  • ProcessPayment
  • SendInvitation
  • ImportTranslations
  • ValidateLicenseKey

Each action has a single public handle() method that takes explicit inputs and returns a result.

PHP

Why Not Services?

I used to use service classes. OrderService with create(), update(), cancel(), refund(). They always grew. OrderService would hit 400 lines with 8 injected dependencies, most of which were only used by one method.

Actions don't have this problem because they only do one thing. When scope grows, you create a new action — not a new method on an existing class.

How I Organize Them

Actions live in app/Actions/ grouped by domain:

Text

Finding code is trivial. Need to know how orders are created? Open CreateOrder.php. Need to change how licenses are validated? Open ValidateLicenseKey.php.

Controllers Become Trivial

With actions, controllers are just HTTP adapters — Form Requests handle validation, actions handle logic:

PHP

The controller does three things: validate, call an action, return a response. Nothing else.

Actions Calling Actions

Complex workflows compose smaller actions:

PHP

Each action is independently testable. The composed action is also testable. You can mock inner actions when testing the outer one, or test the full flow in an integration test.

Testing Actions

Because actions have explicit inputs and outputs, testing is straightforward:

PHP

No HTTP layer, no request objects, no middleware. Pure business logic testing.

Reusability Across Contexts

The same action works in a controller, an Artisan command, a queued job, or a webhook handler:

PHP

Write the logic once, use it everywhere.

When Not to Use Actions

Don't create an action for everything. If the logic is two lines and only used in one place, put it in the controller. An action should exist when:

  • The logic is more than a few lines
  • It could be reused in another context
  • It has dependencies that should be injected
  • It represents a meaningful business operation

Three similar lines in a controller is better than a premature abstraction.

Related articles