The Application Layer: Orchestrating Use Cases with Commands and Queries
The Application layer is the pure orchestrator — it coordinates Domain entities, calls Repository ports, and dispatches Domain Events, but contains zero business rules. This article implements concrete use cases (PlaceOrder, CancelOrder, ProcessRefund) following the Command pattern and demonstrates the discipline of keeping application services thin.
Backend Clean Architecture & Domain-Driven Design
The Application Layer: Orchestrating Use Cases with Commands and Queries
Domain objects are autonomous state machines with enforced invariants — they are not passive bags of data passed between controller functions. The Application layer's job is to be the thinnest possible orchestration shell between the delivery mechanism (HTTP, Kafka, CLI) and the domain model. It loads Aggregates, invokes their methods, persists them, and dispatches Domain Events. It does not calculate business rules, it does not know about Prisma, and it does not parse HTTP requests. If your use case contains an if statement that enforces a domain rule, that rule belongs in the domain — not here.
This article implements the three primary use cases for the billing engine: PlaceOrderUseCase, CancelOrderUseCase, and GetOrderSummaryUseCase. The first two are Commands (they change state). The third is a Query (it reads state). We separate them structurally from the start, laying the foundation for the full CQRS split in Part 11.
Prerequisites: Part 4 (Aggregates & Repositories) for IOrderRepository and the Order Aggregate; Part 5 (Domain Events) for IEventBus and the post-commit dispatch timing rule. The use cases here depend on all three ports: IOrderRepository, IPaymentProcessor, and IEventBus.
1. The Bloated Service Anti-Pattern
Before defining what a use case is, it helps to name what it is not — the pattern it replaces:
This class has at least six reasons to change (HTTP format, discount policy, payment gateway, database schema, email template, caching strategy). Testing it requires a live database, live Stripe, and a live mail server. Adding a Kafka consumer that places orders means either duplicating this code or accepting that the service handles HTTP concerns.
2. Command DTOs: The Application Layer's API Surface
A Command is a plain TypeScript interface that describes what the user intends to do — no HTTP types, no Prisma types, no framework decorations. It is the entry point to the Application layer:
These are value objects in disguise. They carry intent, not state. The HTTP controller constructs them from req.body. The Kafka consumer constructs them from a parsed message. The CLI command constructs them from parsed arguments. The use case receives the same PlaceOrderCommand regardless of delivery mechanism.
Use readonly on every field in Command DTOs. Commands are inputs — they should never be mutated after construction. TypeScript's Readonly<T> utility type or explicit readonly field modifiers enforce this at compile time.
3. PlaceOrderUseCase: The Primary Command
Count the lines of business logic in this use case: zero. order.place() enforces all invariants. Address.create() validates the address. order.addItem() enforces quantity and price guards. The use case is pure orchestration — load, invoke, save, dispatch.
The use case is also completely testable without a database:
Three tests, zero external dependencies, runs in under 10ms.
4. CancelOrderUseCase
Notice the authorization check sits in the Application layer, not in the domain. This is deliberate. Whether a given CustomerId is allowed to cancel OrderId is an authorization policy — it depends on context (admin users can cancel any order, customers can only cancel their own). Authorization is not a domain invariant; it is an application-layer gate.
Domain vs. Application concern boundary: Domain invariants are universal and unconditional — "an Order cannot be shipped without confirmed payment" is true regardless of who is asking. Authorization rules are contextual and policy-driven — "only the owning customer or an admin can cancel an order" depends on the requesting party. Domain invariants live in Aggregate methods. Authorization lives in the Application layer (or a dedicated Policy object).
5. Queries vs. Commands: The CQRS Seed
Commands change state. Queries read state. Mixing them in the same method creates subtle bugs:
The Command Segregation principle (half of CQRS — Part 11 covers the full split) says: commands return only the ID of the created/modified resource; queries return read-optimized view models.
5.1 GetOrderSummaryUseCase: A Read-Optimized Query
The query use case returns an OrderSummaryView — a plain object shaped for the UI. It does not return the Order domain object. The UI cannot accidentally call orderView.cancel() or mutate orderView.items because those methods and the mutable backing array do not exist on the view model.
In Part 11, this query use case is replaced by a direct SQL read against a materialized read model — the GetOrderSummaryUseCase will hit a dedicated order_summary_view table that is continuously updated by an OrderSummaryProjector subscribed to domain events. The Command side and Query side operate on entirely separate data models. The structure set up here makes that migration trivial.
6. Error Handling Strategy at the Application Boundary
The Application layer is where domain errors are translated into HTTP/gRPC/CLI errors. The use case throws typed DomainError instances. The Interface layer catches them and maps them to protocol-appropriate responses:
The error taxonomy is:
HttpBadRequestError(400): malformed request — missing required field, wrong typeDomainError(422): well-formed request that violates a business rule — suspended customer, no items, invalid status transition- Unhandled
Error(500): infrastructure failure — database connection, external API timeout
7. Application Layer Checklist
Before a use case is considered complete, every item must pass:
| Check | Verification |
|---|---|
| No business logic in the use case | grep -n 'if.*order\.' PlaceOrderUseCase.ts should return zero results (all guards are in the domain) |
| No infrastructure imports | grep -n 'prisma|stripe|redis|kafka' PlaceOrderUseCase.ts must be empty |
| All dependencies are interfaces | Constructor parameters use IOrderRepository, not PrismaOrderRepository |
Command fields are readonly |
TypeScript enforces this at compile time |
| Events dispatched post-commit | pullDomainEvents() is called after save(), never before |
| Returns only IDs (Commands) or view models (Queries) | Commands return OrderId, never Order; Queries return OrderSummaryView, never Order |
| Tested with in-memory doubles | No test in tests/unit/ requires a live database |
Summary
| Concept | Domain Rule |
|---|---|
| Use Case | A single, named application action; one execute() method; one responsibility |
| Command DTO | Plain TypeScript interface with readonly fields; no HTTP or ORM types |
| Orchestration Pattern | Load → Invoke domain → Persist → Dispatch events → Return ID |
| Authorization | Application-layer concern; never inside a domain method |
| Command returns ID | Commands return only the created resource's ID, never the full domain object |
| Query returns view model | Queries return a display-ready plain object; the domain Aggregate is never returned |
| No business logic | Every if statement in a use case that enforces a domain rule is a misplacement |
What's Next
In Part 7, we formalize the Port/Adapter vocabulary introduced throughout Parts 2–6 with the Hexagonal Architecture pattern — showing how the same billing engine core is simultaneously driven by HTTP, Kafka, and a CLI without any changes to domain or application code. Part 7: Ports & Adapters →
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.