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:
CreateOrderProcessPaymentSendInvitationImportTranslationsValidateLicenseKey
Each action has a single public handle() method that takes explicit inputs and returns a result.
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:
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:
The controller does three things: validate, call an action, return a response. Nothing else.
Actions Calling Actions
Complex workflows compose smaller actions:
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:
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:
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.