Laravel Patterns
Placement rules for domain-driven Laravel applications: Action, Service, Repository, Query Class, Value Object. 63 rules across 9 sections.
Core philosophy: practicality over purity. Never take a greedy decision.
- Eloquent is already a good abstraction for most data access.
- A Repository over Eloquent does not fully decouple you from Eloquent. That is acceptable and expected — do not chase purity.
- Both extremes are bugs: a layer for every model (dead boilerplate) and none at all (queries scattered everywhere).
- A Query Class is the internal implementation technique of a Repository, not a competing pattern.
- Value Objects keep signatures clean and carry pure behavior. They never touch a
Builder.
- Domains are bounded contexts. One domain never imports another's internals.
When to Apply
Reference these rules when:
- Creating any class under
app/Domain/
- Deciding between a Service, a Repository and inline code
- Reviewing a pull request that adds a layer
- Refactoring queries scattered across Actions, Services or Blade
- Splitting a monolithic
app/ into bounded contexts
- Wiring one domain to another
Start Here
Run the Decision Gate before writing any class: references/decision-gate.md.
Default answer: keep it in the Action, use Eloquent directly.
Q1. Single end-to-end use case? → ACTION
Q2. Called by 2+ Actions, or worth isolating? → SERVICE
Q3. Used in only one Action? → KEEP IT IN THE ACTION
Q4. Single-record CRUD (find / create /
update / delete)? → ELOQUENT DIRECTLY
(from an Action or Repository)
Q5. Backend may swap, OR the query earns a
name and its own tests? → REPOSITORY (+ Query Classes)
Q6. Two real implementations, a concrete
backend swap, or a host extension point? → INTERFACE
(otherwise one concrete class)
Q7. Callers pick one by a config or
request key? → FACTORY / REGISTRY
Ambiguous between Q4 and Q5? Choose Q4 — except for a list endpoint, which is always Q5.
Non-Negotiables
These are the mistakes that survive review because each one looks locally reasonable:
- Query construction never appears in a Controller, Form Request, Resource, Blade view or Middleware — however small the query is.
- A paginated, filtered or ownership-scoped list is a named query behind a Repository, not "simple CRUD".
- An Action that only forwards to one collaborator is deleted; the caller calls the collaborator.
- An interface, its implementation and its container binding land in the same change. Never an empty
Repositories/.
- One concept has one home: a shared module owns the mechanism, each domain owns the content describing its own data.
- Every route is authorized in exactly one place — see the
laravel-rest-api skill.
Pick the Rule
| About to write |
Read |
Any new class under app/Domain/ |
gate-run-decision-gate-first |
| An index / list / search endpoint |
gate-reads-go-through-a-named-query, query-whitelist-sortable-columns |
| A create, update or delete use case |
gate-action-for-use-case, action-one-use-case-end-to-end |
| An Action that only calls one thing |
action-not-a-pass-through |
| A Repository interface |
gate-repository-earns-its-name, repo-ship-implementation-and-binding |
| A query with filters, sorting or eager loads |
query-owns-all-query-construction, query-single-handle-method |
| Logic a second Action now needs |
gate-service-only-when-reused |
| A method with more than four parameters |
vo-more-than-four-params, vo-group-related-parameters |
| Code that touches another domain |
domain-no-cross-domain-models, domain-events-for-reactions |
| A notification, report or export class |
layout-shared-module-owns-mechanism |
| An interface, factory, registry or base class |
gate-earn-extension-seam |
| Behavior only some implementations need |
gate-opt-in-capability-contract |
| A driver, provider or class chosen per deployment |
config-select-deployable-variation |
| Anything reading configuration |
config-never-env-outside-config |
| A sort or filter arriving as a string |
vo-named-constructor-parses-input |
| A file you cannot place |
layout-scope-based-co-location |
Build Order
A vertical slice lands in this order — each step exists only if the step above it earned it:
Contracts/<X>RepositoryInterface.php — the Q5 trigger named in the docblock
Queries/<BusinessQuestion>Query.php — one handle(), all clauses
Repositories/Eloquent<X>Repository.php — implements the interface
- the binding in
DomainServiceProvider — same change, never later
Actions/<Verb><Noun>Action.php — only when state changes
- the edge: Form Request → Controller → Resource (
laravel-rest-api)
- tests per layer (
laravel-testing)
Before You Write Code
- Every API named in these rules is verified against Laravel
^12.0 || ^13.0 and PHP ^8.3. If you need something these rules do not name, check the docs — never infer an API from its name.
- Version-gated APIs are marked inline ("Laravel 13 only"). Read the project's
composer.json first; on Laravel 12 use the fallback the rule gives.
- Where the project already differs from a rule, follow the project. Name the rule you set aside and why, rather than half-converting the codebase.
- When two rules collide, the higher-impact section wins — sections are ordered by impact.
- One example is not the whole rule. Open
rules/{slug}.md before adapting it to a case the example does not show.
Rule Sections by Priority
| # |
Section |
Impact |
Prefix |
| 1 |
The Decision Gate |
CRITICAL |
gate- |
| 2 |
Actions |
HIGH |
action- |
| 3 |
Services |
HIGH |
service- |
| 4 |
Repositories |
HIGH |
repo- |
| 5 |
Query Classes |
HIGH |
query- |
| 6 |
Value Objects and Parameter Isolation |
MEDIUM-HIGH |
vo- |
| 7 |
Directory and Namespace Layout |
MEDIUM |
layout- |
| 8 |
Inter-Domain Communication |
HIGH |
domain- |
| 9 |
Configuration and Environments |
MEDIUM |
config- |
Quick Reference
1. The Decision Gate (CRITICAL)
gate-run-decision-gate-first — Name the trigger before creating any class
gate-action-for-use-case — One use case means one Action
gate-service-only-when-reused — Extract a Service only when two Actions need it
gate-eloquent-directly-by-default — Use Eloquent directly by default
gate-repository-earns-its-name — A Repository must name its trigger
gate-query-class-and-repository-together — Query Classes and Repositories arrive together
gate-reads-go-through-a-named-query — A list endpoint is a named query
gate-earn-extension-seam — Earn interfaces and factories from real variation
gate-opt-in-capability-contract — Optional behavior is a second contract, not a wider one
2. Actions (HIGH)
action-one-use-case-end-to-end — An Action orchestrates one use case end to end
action-keep-single-use-logic-inline — Keep single-use logic inside the Action
action-naming-verb-noun — Name Actions <Verb><Noun>Action
action-maps-request-to-value-objects — Map HTTP input to domain types at the edge
action-not-a-pass-through — Never create an Action that only forwards
3. Services (HIGH)
service-two-or-more-actions — A Service serves two or more Actions
service-stateless-and-focused — Services are stateless and context-agnostic
service-never-imports-query-classes — A Service never imports a Query Class
service-not-a-disguised-repository — A Service whose body is a query is a mislabeled Repository
service-naming-business-decision — Name Services after the business decision
4. Repositories (HIGH)
repo-interface-in-domain-contracts — Interface in Contracts/, implementation in Repositories/
repo-domain-intent-methods — Repository methods express intent, not CRUD
repo-never-returns-builder — A Repository interface never returns a Builder
repo-never-accepts-request — A Repository never accepts a Request
repo-no-base-repository — No generic BaseRepository
repo-small-focused-interface — Keep Repository interfaces under ~6 methods
repo-bind-in-service-provider — Bind the interface in a service provider
repo-ship-implementation-and-binding — Interface, implementation and binding in one change
repo-inline-simple-delegate-complex — Inline simple queries, delegate complex ones
5. Query Classes (HIGH)
query-single-handle-method — Exactly one public method, handle()
query-internal-to-repositories — Query Classes are internal to Repositories
query-name-the-business-question — Name the business question, not the DB operation
query-final-readonly-no-base-class — Plain final readonly, no abstract base
query-can-write — A Query Class may write
query-return-builder-or-execute — Return a Builder or execute, one per query
query-compose-query-classes — Compose instead of duplicating clauses
query-whitelist-sortable-columns — Whitelist sortable columns in the Query Class
query-owns-all-query-construction — All query construction lives here
6. Value Objects and Parameter Isolation (MEDIUM-HIGH)
vo-group-related-parameters — Group contextually related parameters
vo-more-than-four-params — More than four parameters must be grouped
vo-never-touches-builder — A Value Object never touches a Builder
vo-no-single-scalar-wrapper — Never wrap a single unrelated scalar
vo-pass-domain-objects-directly — Pass essential domain objects directly
vo-date-range-and-presets — Express named date ranges through an interface
vo-parameterized-presets — Parameterize presets instead of copying classes
vo-composite-filter-per-query — Collapse a query's inputs into one composite filter
vo-named-constructor-parses-input — Parse the wire format in a named constructor
7. Directory and Namespace Layout (MEDIUM)
layout-domain-first-structure — Organize by domain, not by layer
layout-scope-based-co-location — Scope decides placement
layout-no-top-level-service-repository-query — No top-level layer folders
layout-optional-folders-are-deliberate — A missing folder is a decision
layout-contracts-vs-support — Contracts/ holds interfaces, Support/ holds implementations
layout-shared-module-owns-mechanism — A shared module owns the mechanism, not other domains' messages
8. Inter-Domain Communication (HIGH)
domain-public-vs-private-surface — A domain has a public surface and a private one
domain-no-cross-domain-models — Never import another domain's Models, Repositories or Queries
domain-events-for-reactions — Use Domain Events for cross-domain reactions
domain-open-host-service-for-sync — Use an Open Host Service for synchronous reads
domain-shared-kernel-concepts-only — The Shared Kernel holds concepts, never calls
domain-anti-corruption-layer — Wrap external upstreams in an Anti-Corruption Layer
9. Configuration and Environments (MEDIUM)
config-secrets-in-env-structure-in-config — Secrets in .env, structure in config/
config-select-deployable-variation — Use config for deploy-time implementation choices
config-never-env-outside-config — Never call env() outside config/
config-per-environment-overrides — Override per environment, not per branch
config-cache-in-production — Cache config, routes and events in production
Reference Material
Read on demand — do not load all of these at once:
references/decision-gate.md — the gate, layer definitions, who may call what
references/directory-layout.md — full tree, folder meanings, CI guards
references/inter-domain-decision-guide.md — picking Event vs Open Host Service vs Shared Kernel vs ACL
references/anti-patterns.md — 18 forbidden patterns with their grep signals
references/pre-completion-checklist.md — 31-question self-check before declaring done
examples/orders-domain/ — one worked vertical slice with every layer in place
How to Use
Load in this order and stop when the answer is clear:
- This file — the Quick Reference names every rule, and usually settles the question.
- One rule file for the reasoning and both examples (~405 tokens each):
rules/gate-eloquent-directly-by-default.md
rules/query-internal-to-repositories.md
- A
references/ file only when a rule points at one.
AGENTS.md is every rule compiled into one document (~22k tokens), for agents that read the AGENTS.md convention. Do not load it when the individual rule files are reachable.
Related Skills
laravel-engineering — context tracing, reuse discipline and critical-path verification
laravel-eloquent — what goes inside a Query Class: casts, scopes, N+1, pagination, transactions, raw SQL
laravel-rest-api — the edge: routing, binding, form requests, resources, authorization, error mapping
laravel-async — events, queued jobs, caching and scheduling
laravel-testing — how to test each layer defined here
1---2name: laravel-patterns3description: Placement rules for domain-driven Laravel applications — when to create an Action, Service, Repository, Query Class or Value Object, and when to just use Eloquent. Use when writing, reviewing or refactoring Laravel code that touches application structure, data access, domain boundaries or class placement. Triggers on questions like "where should this live", "should this be a service", "do I need a repository", or any new class in app/Domain.4license: MIT5---67# Laravel Patterns89Placement rules for domain-driven Laravel applications: Action, Service, Repository, Query Class, Value Object. 63 rules across 9 sections.1011**Core philosophy: practicality over purity. Never take a greedy decision.**1213- Eloquent is already a good abstraction for most data access.14- A Repository over Eloquent does not fully decouple you from Eloquent. That is acceptable and expected — do not chase purity.15- Both extremes are bugs: a layer for every model (dead boilerplate) *and* none at all (queries scattered everywhere).16- A Query Class is the internal implementation technique of a Repository, not a competing pattern.17- Value Objects keep signatures clean and carry pure behavior. They never touch a `Builder`.18- Domains are bounded contexts. One domain never imports another's internals.1920## When to Apply2122Reference these rules when:2324- Creating any class under `app/Domain/`25- Deciding between a Service, a Repository and inline code26- Reviewing a pull request that adds a layer27- Refactoring queries scattered across Actions, Services or Blade28- Splitting a monolithic `app/` into bounded contexts29- Wiring one domain to another3031## Start Here3233Run the Decision Gate before writing any class: `references/decision-gate.md`.3435**Default answer: keep it in the Action, use Eloquent directly.**3637```38Q1. Single end-to-end use case? → ACTION39Q2. Called by 2+ Actions, or worth isolating? → SERVICE40Q3. Used in only one Action? → KEEP IT IN THE ACTION41Q4. Single-record CRUD (find / create /42 update / delete)? → ELOQUENT DIRECTLY43 (from an Action or Repository)44Q5. Backend may swap, OR the query earns a45 name and its own tests? → REPOSITORY (+ Query Classes)46Q6. Two real implementations, a concrete47 backend swap, or a host extension point? → INTERFACE48 (otherwise one concrete class)49Q7. Callers pick one by a config or50 request key? → FACTORY / REGISTRY51```5253Ambiguous between Q4 and Q5? Choose Q4 — except for a list endpoint, which is always Q5.5455## Non-Negotiables5657These are the mistakes that survive review because each one looks locally reasonable:5859- Query construction never appears in a Controller, Form Request, Resource, Blade view or Middleware — however small the query is.60- A paginated, filtered or ownership-scoped list is a named query behind a Repository, not "simple CRUD".61- An Action that only forwards to one collaborator is deleted; the caller calls the collaborator.62- An interface, its implementation and its container binding land in the same change. Never an empty `Repositories/`.63- One concept has one home: a shared module owns the mechanism, each domain owns the content describing its own data.64- Every route is authorized in exactly one place — see the `laravel-rest-api` skill.6566## Pick the Rule6768| About to write | Read |69|----------------|------|70| Any new class under `app/Domain/` | `gate-run-decision-gate-first` |71| An index / list / search endpoint | `gate-reads-go-through-a-named-query`, `query-whitelist-sortable-columns` |72| A create, update or delete use case | `gate-action-for-use-case`, `action-one-use-case-end-to-end` |73| An Action that only calls one thing | `action-not-a-pass-through` |74| A Repository interface | `gate-repository-earns-its-name`, `repo-ship-implementation-and-binding` |75| A query with filters, sorting or eager loads | `query-owns-all-query-construction`, `query-single-handle-method` |76| Logic a second Action now needs | `gate-service-only-when-reused` |77| A method with more than four parameters | `vo-more-than-four-params`, `vo-group-related-parameters` |78| Code that touches another domain | `domain-no-cross-domain-models`, `domain-events-for-reactions` |79| A notification, report or export class | `layout-shared-module-owns-mechanism` |80| An interface, factory, registry or base class | `gate-earn-extension-seam` |81| Behavior only some implementations need | `gate-opt-in-capability-contract` |82| A driver, provider or class chosen per deployment | `config-select-deployable-variation` |83| Anything reading configuration | `config-never-env-outside-config` |84| A sort or filter arriving as a string | `vo-named-constructor-parses-input` |85| A file you cannot place | `layout-scope-based-co-location` |8687## Build Order8889A vertical slice lands in this order — each step exists only if the step above it earned it:90911. `Contracts/<X>RepositoryInterface.php` — the Q5 trigger named in the docblock922. `Queries/<BusinessQuestion>Query.php` — one `handle()`, all clauses933. `Repositories/Eloquent<X>Repository.php` — implements the interface944. the binding in `DomainServiceProvider` — same change, never later955. `Actions/<Verb><Noun>Action.php` — only when state changes966. the edge: Form Request → Controller → Resource (`laravel-rest-api`)977. tests per layer (`laravel-testing`)9899## Before You Write Code100101- Every API named in these rules is verified against Laravel `^12.0 || ^13.0` and PHP `^8.3`. If you need something these rules do not name, check the docs — never infer an API from its name.102- Version-gated APIs are marked inline ("Laravel 13 only"). Read the project's `composer.json` first; on Laravel 12 use the fallback the rule gives.103- Where the project already differs from a rule, follow the project. Name the rule you set aside and why, rather than half-converting the codebase.104- When two rules collide, the higher-impact section wins — sections are ordered by impact.105- One example is not the whole rule. Open `rules/{slug}.md` before adapting it to a case the example does not show.106107## Rule Sections by Priority108109| # | Section | Impact | Prefix |110|---|---------|--------|--------|111| 1 | The Decision Gate | CRITICAL | `gate-` |112| 2 | Actions | HIGH | `action-` |113| 3 | Services | HIGH | `service-` |114| 4 | Repositories | HIGH | `repo-` |115| 5 | Query Classes | HIGH | `query-` |116| 6 | Value Objects and Parameter Isolation | MEDIUM-HIGH | `vo-` |117| 7 | Directory and Namespace Layout | MEDIUM | `layout-` |118| 8 | Inter-Domain Communication | HIGH | `domain-` |119| 9 | Configuration and Environments | MEDIUM | `config-` |120121## Quick Reference122123### 1. The Decision Gate (CRITICAL)124125- `gate-run-decision-gate-first` — Name the trigger before creating any class126- `gate-action-for-use-case` — One use case means one Action127- `gate-service-only-when-reused` — Extract a Service only when two Actions need it128- `gate-eloquent-directly-by-default` — Use Eloquent directly by default129- `gate-repository-earns-its-name` — A Repository must name its trigger130- `gate-query-class-and-repository-together` — Query Classes and Repositories arrive together131- `gate-reads-go-through-a-named-query` — A list endpoint is a named query132- `gate-earn-extension-seam` — Earn interfaces and factories from real variation133- `gate-opt-in-capability-contract` — Optional behavior is a second contract, not a wider one134135### 2. Actions (HIGH)136137- `action-one-use-case-end-to-end` — An Action orchestrates one use case end to end138- `action-keep-single-use-logic-inline` — Keep single-use logic inside the Action139- `action-naming-verb-noun` — Name Actions `<Verb><Noun>Action`140- `action-maps-request-to-value-objects` — Map HTTP input to domain types at the edge141- `action-not-a-pass-through` — Never create an Action that only forwards142143### 3. Services (HIGH)144145- `service-two-or-more-actions` — A Service serves two or more Actions146- `service-stateless-and-focused` — Services are stateless and context-agnostic147- `service-never-imports-query-classes` — A Service never imports a Query Class148- `service-not-a-disguised-repository` — A Service whose body is a query is a mislabeled Repository149- `service-naming-business-decision` — Name Services after the business decision150151### 4. Repositories (HIGH)152153- `repo-interface-in-domain-contracts` — Interface in `Contracts/`, implementation in `Repositories/`154- `repo-domain-intent-methods` — Repository methods express intent, not CRUD155- `repo-never-returns-builder` — A Repository interface never returns a `Builder`156- `repo-never-accepts-request` — A Repository never accepts a `Request`157- `repo-no-base-repository` — No generic `BaseRepository`158- `repo-small-focused-interface` — Keep Repository interfaces under ~6 methods159- `repo-bind-in-service-provider` — Bind the interface in a service provider160- `repo-ship-implementation-and-binding` — Interface, implementation and binding in one change161- `repo-inline-simple-delegate-complex` — Inline simple queries, delegate complex ones162163### 5. Query Classes (HIGH)164165- `query-single-handle-method` — Exactly one public method, `handle()`166- `query-internal-to-repositories` — Query Classes are internal to Repositories167- `query-name-the-business-question` — Name the business question, not the DB operation168- `query-final-readonly-no-base-class` — Plain `final readonly`, no abstract base169- `query-can-write` — A Query Class may write170- `query-return-builder-or-execute` — Return a `Builder` or execute, one per query171- `query-compose-query-classes` — Compose instead of duplicating clauses172- `query-whitelist-sortable-columns` — Whitelist sortable columns in the Query Class173- `query-owns-all-query-construction` — All query construction lives here174175### 6. Value Objects and Parameter Isolation (MEDIUM-HIGH)176177- `vo-group-related-parameters` — Group contextually related parameters178- `vo-more-than-four-params` — More than four parameters must be grouped179- `vo-never-touches-builder` — A Value Object never touches a `Builder`180- `vo-no-single-scalar-wrapper` — Never wrap a single unrelated scalar181- `vo-pass-domain-objects-directly` — Pass essential domain objects directly182- `vo-date-range-and-presets` — Express named date ranges through an interface183- `vo-parameterized-presets` — Parameterize presets instead of copying classes184- `vo-composite-filter-per-query` — Collapse a query's inputs into one composite filter185- `vo-named-constructor-parses-input` — Parse the wire format in a named constructor186187### 7. Directory and Namespace Layout (MEDIUM)188189- `layout-domain-first-structure` — Organize by domain, not by layer190- `layout-scope-based-co-location` — Scope decides placement191- `layout-no-top-level-service-repository-query` — No top-level layer folders192- `layout-optional-folders-are-deliberate` — A missing folder is a decision193- `layout-contracts-vs-support` — `Contracts/` holds interfaces, `Support/` holds implementations194- `layout-shared-module-owns-mechanism` — A shared module owns the mechanism, not other domains' messages195196### 8. Inter-Domain Communication (HIGH)197198- `domain-public-vs-private-surface` — A domain has a public surface and a private one199- `domain-no-cross-domain-models` — Never import another domain's Models, Repositories or Queries200- `domain-events-for-reactions` — Use Domain Events for cross-domain reactions201- `domain-open-host-service-for-sync` — Use an Open Host Service for synchronous reads202- `domain-shared-kernel-concepts-only` — The Shared Kernel holds concepts, never calls203- `domain-anti-corruption-layer` — Wrap external upstreams in an Anti-Corruption Layer204205### 9. Configuration and Environments (MEDIUM)206207- `config-secrets-in-env-structure-in-config` — Secrets in `.env`, structure in `config/`208- `config-select-deployable-variation` — Use config for deploy-time implementation choices209- `config-never-env-outside-config` — Never call `env()` outside `config/`210- `config-per-environment-overrides` — Override per environment, not per branch211- `config-cache-in-production` — Cache config, routes and events in production212213## Reference Material214215Read on demand — do not load all of these at once:216217- `references/decision-gate.md` — the gate, layer definitions, who may call what218- `references/directory-layout.md` — full tree, folder meanings, CI guards219- `references/inter-domain-decision-guide.md` — picking Event vs Open Host Service vs Shared Kernel vs ACL220- `references/anti-patterns.md` — 18 forbidden patterns with their grep signals221- `references/pre-completion-checklist.md` — 31-question self-check before declaring done222- `examples/orders-domain/` — one worked vertical slice with every layer in place223224## How to Use225226Load in this order and stop when the answer is clear:2272281. This file — the Quick Reference names every rule, and usually settles the question.2292. One rule file for the reasoning and both examples (~405 tokens each):230231```232rules/gate-eloquent-directly-by-default.md233rules/query-internal-to-repositories.md234```2352363. A `references/` file only when a rule points at one.237238`AGENTS.md` is every rule compiled into one document (~22k tokens), for agents that read the AGENTS.md convention. Do not load it when the individual rule files are reachable.239240## Related Skills241242- `laravel-engineering` — context tracing, reuse discipline and critical-path verification243- `laravel-eloquent` — what goes *inside* a Query Class: casts, scopes, N+1, pagination, transactions, raw SQL244- `laravel-rest-api` — the edge: routing, binding, form requests, resources, authorization, error mapping245- `laravel-async` — events, queued jobs, caching and scheduling246- `laravel-testing` — how to test each layer defined here