Utilitarian Architecture is an approach to application architecture that prioritizes usefulness, clarity, and real-world outcomes over theoretical purity. It values clean code and good design principles, but treats them as tools — not goals.
Decisions are made based on their practical impact on the system, the team, and the problem being solved. Patterns, abstractions, and layers are applied consciously, only when they increase overall utility.
What matters is not how elegant a structure looks, but how useful it is in practice.
Architecture is a means of achieving outcomes.
Software is an implementation detail.
The basic unit of the architecture is a business operation, not a layer or a technical pattern.
Command — an atomic write operation.
Query — a read scenario.
Action — orchestration and application logic: calculations, transformations, conditions, and coordination of commands and queries.
Command, Query, and Action are not required together — each is used only when it makes sense for the task at hand.
Read and write responsibilities are separated intentionally, but without turning CQRS into an architectural framework.
Command / Query / Action are clear conventions that improve readability and predictability, while still allowing each problem to be solved in the most reasonable way.
The architecture does not fix the technical level at which a problem must be solved.
Inside commands, queries, and actions, it is acceptable to work with Eloquent, use the Query Builder, call external APIs, use SQL, HTTP, SDKs, file systems, or any other appropriate tools.
What matters is not how the task is implemented, but where it is defined and which business meaning it expresses. A use case is a responsibility boundary, not an abstraction layer.
Utilitarian Architecture requires nothing by default.
Value Objects, DTOs, notifications, lightweight helpers, and custom utilities are not architectural obligations, but ready-to-use tools for frequently recurring problems.
The key idea is simple:
if a tool reduces cognitive load, removes duplication, and fits naturally into the codebase — it is worth adding.
Each with a clearly defined responsibility within business operations
Exclusively for write operations: creating, updating, and deleting data. Each command represents a single atomic business operation like creating an order or confirming a payment. No data retrieval for presentation.
CreateOrderCommand
ConfirmPaymentCommand
Exclusively for reading data with no side effects. Perform complex joins, eager loading, and data aggregation. Return data in the form required by the caller—models, collections, arrays, or DTOs. The read-side use-case layer.
GetOrderDetailsQuery
ListProductsQuery
Encapsulate application logic that doesn't fit into commands or queries. Used for multi-step business scenarios requiring orchestration, conditions, validations, and branching logic. Always executed as a single complete operation.
PlaceOrderAction
ProcessReturnAction
Transform primitive values into meaningful domain concepts with guaranteed invariants. Fail-fast validation at construction ensures data is either always valid or doesn't exist. No repeated validation needed throughout the system.
Email
Money
PostalCode
Callable classes that accept and return a Laravel Query Builder. Encapsulate reusable query fragments like conditions, joins, and filters. Work at a lower abstraction level—conscious mixing that embraces the Query Builder as an effective tool.
WhereActiveModifier
WithPaginationModifier
The HTTP layer handles requests and responses only. Core business logic never lives here.
Controllers are optional. Routes may directly delegate execution to application-level Actions, Commands, or Queries, either through controllers or minimal route closures used purely as entry points.
Controllers, when used, act as transport adapters for validation, authorization, middleware interaction, and response formatting. All business rules remain isolated in application-level objects, which may return data or, when appropriate, an HTTP response.
The system is designed around use cases, not controllers.