Orchestration Patterns and Naming
This page is a reference for building a REST resource from function orchestration functions. It collects the naming conventions and the data flow rules. The REST CRUD Orchestration tutorial applies them to a working resource.
A function has one of three roles. A producer obtains an entity and publishes it. An action changes an entity. A responder turns an entity into a response. Every endpoint is a chain of producers and actions that ends in a responder.
From layers to a pipeline
A traditional web application has three layers. A presentation layer accepts the request and returns the response. A service layer holds the business logic. A data layer reaches the database. This shape is not a law. It is a consequence of how method calls work.
A method call is a round trip. The caller calls down into the callee and waits. The callee returns, and the caller continues. So any work that must happen after a deeper call has to sit in the caller, on the way back up. Each layer becomes a sandwich. On the way down it prepares. On the way up it finishes. The controller maps the request DTO on the way down and maps the response DTO on the way up. The service opens a transaction on the way down and commits it on the way up. Errors are caught and translated on the way up. The layers exist to organise this down and up traffic.
Function orchestration removes the round trip. The functions run once, in order, and hand off forward. There is no way back up. So the sandwich disappears, and with it the reason for the layers.
| Layered application | Function orchestration |
|---|---|
| Controller maps the request DTO, then calls down | Build or Validate function at the front edge |
| Service business logic | Apply and Resolve functions in the middle |
| Repository call, the bottom of the stack | Load or Save function, in line with the rest |
| Service opens a transaction, commits on the return | govern across the functions, commit at the end of the request |
| Controller maps the entity to the response DTO on the return | RespondWith function at the back edge, a forward function |
| Exceptions unwind up through the layers | a function throws, and the exception escalates to a handler |
The vertical stack becomes a horizontal line. A layered update reads across three classes. The controller calls the service. The service loads and saves through the repository. The mapping and the response happen as the calls unwind. The same update as a pipeline reads top to bottom in one file: validate, load, apply, save, respond. The response is the last forward function, not the unwinding of a controller.
A little of the old shape remains, and only at the edges. The front functions take the request DTO. The back function produces the response DTO. That is the presentation boundary. But it is the two ends of a line, not a layer that wraps the others. The middle functions work with entities, which resembles the service layer. But it is the body of the pipeline, not a layer that is called and returned from. There is no separate data layer at all. A repository is a dependency of a function, and persistence is a function in the line.
So the layer boundary becomes a data boundary. DTOs live only at the edges. Entities live in the middle. Each function declares what it works with, and that declaration is the boundary. It is enforced by the data, not by a call.
Function naming
Each function is a single class with one public method. The class name states the role. The reader can pick the right class from its name alone.
| Function | Role | Shape |
|---|---|---|
Load<Entity> |
producer | Path variable in. Publishes Out<Entity>. Throws NotFoundException when absent. |
Build<Entity> |
producer | @Valid @RequestBody in. Publishes Out<Entity>. Constructs a new entity. |
Validate<Entity> |
producer | @Valid @RequestBody in. Publishes Out<Dto>. Stashes the validated body for a later function. |
Apply<Entity> |
action | @Val Entity and @Val Dto in. Mutates the entity in place. |
Save<Entity> |
action | @Val Entity in. Persists with repository.save. |
Delete<Entity> |
action | @Val Entity in. Deletes with repository.delete. |
RespondWith<Entity> |
responder | @Val Entity in. Sends 200 with the response DTO. |
RespondWith<Entity>Created |
responder | @Val Entity in. Sends 201 with the response DTO. |
RespondWithNoContent |
responder | Sends 204. Takes no entity. Shared by every no-content endpoint. |
The name pairs a verb with the entity. Load and Build are the two ways to get an entity into the pipeline. Save and RespondWith read one letter apart at a glance, so the responder uses the longer RespondWith prefix. The status sits in the responder name. So the pipeline reads its response contract from the function names.
DTOs only at the edges
A function in the middle of a pipeline works only with entities. The DTO appears twice, and only at the edges. It arrives as the request body at the first function. It leaves as the response body at the responder. Between those two points the entity is the only currency.
This keeps each function simple. A Save function saves an entity. It does not know about DTOs, status codes or JSON. A RespondWith function is the one place the entity to DTO mapping lives.
Passing state with variables
Functions do not call each other. A producer publishes a value into a variable. A later function consumes it. The variable type is the contract.
A producer declares Out<T> and calls set. A consumer declares @Val T. OfficeFloor matches them by type. For a request that handles one entity type there is one variable of that type, so the match is unambiguous.
@Val returns the same object the producer stored. It is not a copy. So an Apply function can mutate a @Val entity in place, and a later Save or RespondWith function sees the change. This is why the update pipeline works without threading the entity through return values.
One request body per pipeline
The HTTP request body can be read only once. Only one function may bind @RequestBody. A second function that also binds it fails at runtime.
When more than one function needs the body, the first function reads it and publishes it as a variable. Later functions read the variable, not the body. This is the job of the Validate function. It binds @Valid @RequestBody once, then publishes Out<Dto> for the Apply function.
Validation runs before the function body
@Valid on a parameter runs before the method body of that function. So the position of the validating function decides when validation happens.
Put the validating function first. If Load ran first, an invalid body against a missing id would return 404 before the body was checked. With Validate first, the invalid body returns 400. This is why create validates in Build (the first function) and update validates in Validate (before Load).
Errors escalate to a handler
A function reports an error by throwing. It does not build the error response itself. The exception escalates to a handler.
Register a handler for the exception type under officefloor/escalation/. Escalation is most specific first. A handler for NotFoundException is used ahead of a handler for Exception. A domain exception and a framework exception are handled the same way. The CRUD tutorial maps NotFoundException to 404 and MethodArgumentNotValidException to 400 with two small handlers.
See the Exception tutorial for escalation in depth.
One public method per class
Each function class has one public method. OfficeFloor detects it, so the YAML names only the class:
load:
class: com.example.LoadArticle
govern: [ readonly-transaction ]
next: respond
There is no method: line. A function name in the YAML is a label. The next value points to another label. The label carries no data. Data flows through variables, not through the wiring.
Governance per function
Each function lists its governance. A read uses readonly-transaction. A write uses transaction. The governance spans the functions of the request, so the whole pipeline runs in one transaction.

