Laravel makes it easy to place business logic in controllers, models, jobs and listeners. That speed helps until the same operation appears in HTTP, a queue worker and a command. Actions, Services and Repositories create clean boundaries only when each abstraction has a clear job.
The short decision rule
- Action: one use case with explicit input and result, such as approving an invoice.
- Service: a cohesive capability shared by use cases, such as pricing or document rendering.
- Repository: a boundary around complex persistence or an external data source, not every Eloquent call.
Controllers should translate HTTP input, invoke a use case and return a response. Keep validation in Form Requests and authorization in policies or explicit checks so the operation remains callable without HTTP.
Actions: one business operation
Actions fit verbs such as CreateSubscription or CancelOrder. Give them typed input, clear transaction ownership and a useful result. Jobs and controllers can then share orchestration. Do not extract every three-line update: add the boundary when rules, side effects, multiple callers or independent tests justify it.
Services: a stable capability
A Service groups related behavior broader than one command: tax calculation, payment-provider access, PDF generation or identity matching. Keep its public API small. An OrderService with twenty unrelated methods is usually a controller in disguise. Narrow vendor adapters also make failures and contract tests manageable.
This supports the modular approach in structuring a large Laravel application without chaos.
Repositories: real persistence boundaries
Eloquent is already an expressive persistence interface. A repository adds value for complex reusable queries, multiple backends, remote APIs, aggregate persistence or a domain that must not depend on Eloquent. It adds little when methods only forward to find() and save(). Prefer scopes or focused query objects for read filters.
Transactions, queues and traceability
The use-case boundary should own the transaction. Dispatch jobs after commit when workers need new records. Make external calls idempotent where practical, attach correlation IDs, cap retries and redact secrets and personal data from logs. Avoid holding a database transaction open while waiting on a slow third-party API.
Common mistakes
- creating interfaces and repositories for every model
- putting validation and authorization in generic Services
- hiding dependencies behind static helpers
- building “god services” grouped only by model
- starting transactions at several nested layers
- dispatching work before commit
- mocking every internal class instead of testing behavior
- adopting patterns without documenting their purpose
Practical checklist
- name the use case and owner
- define typed inputs, outputs and failures
- keep transport outside business logic
- use Actions for important verbs
- use Services for cohesive shared capabilities
- add Repositories only at meaningful data boundaries
- make transaction and retry ownership explicit
- log correlation without sensitive content
- test rules and critical integrations
- remove abstractions that only forward calls
When hiring a technical person makes sense
A senior Laravel engineer helps when changes cause regressions across controllers, jobs and commands, transaction ownership is unclear, or dependency direction cannot be explained. Start with evidence rather than a rewrite; see auditing Laravel before investing in refactoring.
Final takeaway
Actions describe what the application does, Services provide reusable capabilities, and Repositories isolate data only when that isolation has value. For practical boundaries instead of ceremonial architecture, review my technical services or contact me.