This is the complete model for declaring Spring Boot REST endpoints as OfficeFloor YAML files. Every endpoint's structure — its functions, their order, and their conditional branches — is explicit in one file, which is what makes endpoints reliable targets for AI coding tools.
Endpoints are YAML files placed under src/main/resources/officefloor/rest/. The file name
encodes both the HTTP method and the URL path. The naming convention is {path}.{METHOD}.yml:
officefloor/rest/
├── greeting.GET.yml → GET /greeting
└── greeting/
├── {name}.GET.yml → GET /greeting/{name}
├── entity/
│ └── {name}.GET.yml → GET /greeting/entity/{name}
├── formal/
│ └── {name}.GET.yml → GET /greeting/formal/{name}
└── {style}/
└── {name}.GET.yml → GET /greeting/{style}/{name}
Rules:
- Directory structure below
officefloor/rest/becomes the URL path — deeper URLs are produced by nesting files in sub-directories. - Curly-brace segments such as
{name}become URL path parameters. - The special filename
index.{METHOD}.ymlmaps to the root path/.
On start-up the starter scans the classpath for these YAML files and registers each one as a handler for its HTTP method and URL path. No additional Java or XML configuration is needed.
Inside each YAML file, top-level entries are named functions. The label on each entry is a developer-chosen name used to wire functions together — it is not a keyword. A function identifies the Java class that implements it:
myLabel:
class: com.example.MyLogicThe first entry in the file is always called when the HTTP request arrives.
When a class has only one public method, that method is used automatically.
When a class has more than one public method, OfficeFloor cannot determine which to call and the application fails to start with:
Require configuring method for service (GreetingStyleLogic) as it contains
multiple public methods (casual, formal)
Every YAML entry that references such a class must include method: to name which method to
invoke:
# greeting/formal/{name}.GET.yml
service:
class: net.officefloor.tutorial.springresthttpserver.GreetingStyleLogic
method: formal# greeting/casual/{name}.GET.yml
service:
class: net.officefloor.tutorial.springresthttpserver.GreetingStyleLogic
method: casualBoth entries reference the same class but each picks a different method.
Use next: to chain to the next function unconditionally after the current function completes:
service:
class: net.officefloor.tutorial.catshttpserver.ServiceLogic
method: service
next: send
send:
class: net.officefloor.tutorial.catshttpserver.ServiceLogic
method: sendThe value a handler method returns becomes the input to the next: function. The receiving
method declares a parameter annotated with @Parameter (from
net.officefloor.plugin.section.clazz.Parameter) to receive it. Returning a value plus next:
is the lightweight way to pass data downstream when there is no branching:
// The function with `next: save` returns a PricedOrder ...
public class CalculatePricingLogic {
public PricedOrder price(@Parameter OrderRequest order, PricingService pricingService) {
double total = pricingService.calculateTotal(order.getProductId(), order.getQuantity());
return new PricedOrder(order.getProductId(), order.getQuantity(), total);
}
}
// ... and the `save` function receives it as an @Parameter
public class SaveOrderLogic {
public void save(@Parameter PricedOrder order, OrderService orderService,
ObjectResponse<OrderResponse> response) {
String orderId = orderService.createOrder(order.getProductId(), order.getQuantity(), order.getTotal());
response.send(new OrderResponse(orderId, order.getProductId(), order.getQuantity(), order.getTotal()));
}
}Only the type matters for the wiring: the return type of one function is matched to the @Parameter
type of the next.
A function may declare named outputs. Each output maps a branch name to the function to run when the handler triggers that output — enabling conditional flow:
validate:
class: net.officefloor.tutorial.springrestfunction.ValidateOrderLogic
outputs:
valid: price
price:
class: net.officefloor.tutorial.springrestfunction.CalculatePricingLogic
next: save
save:
class: net.officefloor.tutorial.springrestfunction.SaveOrderLogicHere validate continues to price only via its valid output; price then always continues
to save. The whole flow — validate, then price, then save — is readable without opening any
Java file.
The output name in the YAML (valid) is not magic — it is matched to a flow declared in the
handler. A flow is a custom @FunctionalInterface parameter annotated with @Flow (from
net.officefloor.plugin.section.clazz.Flow), where the annotation value is the output name. The
handler triggers the branch by calling the interface's method; the argument passed becomes the
@Parameter of the receiving function:
public class ValidateOrderLogic {
// Custom functional interface = the "valid" branch. Its argument type (OrderRequest)
// becomes the @Parameter of the target function (price).
@FunctionalInterface
public interface ValidOrderFlow {
void flow(OrderRequest order);
}
public void service(
@RequestBody OrderRequest request,
@Flow("valid") ValidOrderFlow validFlow, // maps to `outputs: { valid: price }`
ObjectResponse<OrderResponse> response) {
if (request.getProductId() == null || request.getProductId().isBlank()
|| request.getQuantity() <= 0) {
// Invalid: respond directly and short-circuit — `price`/`save` never run.
response.send(new OrderResponse(null, request.getProductId(), request.getQuantity(), 0.0));
} else {
// Valid: route to whatever function `valid` is mapped to in the YAML (here, price).
validFlow.flow(request);
}
}
}Key points:
@Flow("valid")binds the parameter to the YAML output namedvalid; the class name of the functional interface (ValidOrderFlow) is arbitrary.- Calling
validFlow.flow(request)transfers execution to the mapped function. The argument (request) arrives there as an@Parameter. - A function can declare several
@Flowparameters for several outputs, and simply not call the ones whose branches should not run — that is how conditional and short-circuit routing is expressed. - This keeps each class ignorant of the others:
ValidateOrderLogicnever namesCalculatePricingLogic. The YAMLoutputs:map is the single place the wiring lives.
Exceptions (called escalations in OfficeFloor) are handled with the same function-injection
model as outputs: and next:: a handler is a plain Java class whose method receives the
routed value as an @Parameter and writes the response with ObjectResponse. The one
difference is how the branch is triggered — a function does not call a @Flow method, it simply
throws the exception, and OfficeFloor routes it to the matching handler.
For OfficeFloor to discover and route an escalation, the exception must be a checked
exception so that it appears in the method's throws clause. That throws clause is how the
YAML wiring is validated at start-up:
public class MockException extends Exception {
public MockException(String message) {
super(message);
}
}The service function just declares and throws it — it names no handler:
public class MethodService {
public void service() throws MockException {
throw new MockException("thrown");
}
}The thrown exception is passed to the handler exactly like any other function input — via
@Parameter (from net.officefloor.plugin.section.clazz.Parameter). This is the same
annotation used to receive a next: return value or a @Flow argument; for an escalation the
value is the thrown exception object:
public class MethodExceptionHandler {
public void handle(@Parameter MockException ex, ObjectResponse<String> response) {
response.send("Method handled: " + ex.getMessage());
}
}No Spring-specific annotations are needed. The handler can return a ResponseEntity (via
ObjectResponse<ResponseEntity<T>>) to set the HTTP status and a ProblemDetail body.
The Java classes are written identically regardless of which level catches the exception — the level is chosen entirely in YAML.
1. Method escalation — declared under the function that throws, applies to that function only:
service:
class: net.officefloor.tutorial.springrestexceptionhttpserver.MethodService
escalations:
net.officefloor.tutorial.springrestexceptionhttpserver.MockException: handler
handler:
class: net.officefloor.tutorial.springrestexceptionhttpserver.MethodExceptionHandler2. Composition escalation — declared in a composition: block at the top of the file,
applies to every function in that file:
composition:
escalations:
net.officefloor.tutorial.springrestexceptionhttpserver.MockException: handler
service:
class: net.officefloor.tutorial.springrestexceptionhttpserver.CompositionService
handler:
class: net.officefloor.tutorial.springrestexceptionhttpserver.CompositionExceptionHandler3. Global escalation — application-wide, one file per exception type under
officefloor/escalation/, the file name being the fully qualified exception class name. Endpoint
YAMLs need no escalation config; the global handler wires automatically. This is the preferred
OfficeFloor-native replacement for Spring's @RestControllerAdvice:
# File: officefloor/escalation/com.example.EscalationNotFoundException.yml
handle:
class: com.example.GlobalExceptionHandler
method: handleNotFoundWhen one handler class serves several exception types, use method: in each escalation file to
pick the method. Global escalation also catches exceptions thrown by governance (e.g. a
TransactionSystemException at transaction commit), since governance failures route through the
same mechanism.
Precedence: method escalation → composition escalation → global escalation. An endpoint can always override a global handler by declaring its own.
If no method, composition, or global escalation matches, the exception propagates out of the
OfficeFloor composition and is handled by Spring's standard
@RestControllerAdvice / @ExceptionHandler infrastructure. This lets OfficeFloor endpoints
participate in an existing Spring application's exception handling with no extra config. Prefer
global escalation for new applications; use the Spring fall-through when integrating with
existing @ControllerAdvice handlers.
Wrap a function's execution with governance — such as a database transaction or auditing — using a
govern: list. Governance is named once and applied per function:
# apply a transaction around the function
service:
class: net.officefloor.tutorial.springrestdatajpa.CreateArticleService
govern: [ transaction ]# apply audit governance around the function
service:
class: net.officefloor.tutorial.springrestgovernance.GovernedService
govern: [ audit ]Two governances exist without any configuration. The starter registers them against Spring's
transaction manager, so there is no file to write under officefloor/govern/:
transaction— a read-write transaction. Use for writes.readonly-transaction— a read-only transaction. Use for reads.
# officefloor/rest/article/{id}.GET.yml
load:
class: com.example.LoadArticle
govern: [ readonly-transaction ]
next: respond
respond:
class: com.example.RespondWithArticle
govern: [ readonly-transaction ]List the governance on every function it covers. Because governance spans the functions of the
request rather than nesting inside a call, the whole pipeline runs in one transaction and commits at
the end of the request — this is what replaces @Transactional on a service method. A failure at
commit escalates like any other exception, so a global escalation handler can catch a
TransactionSystemException.
The two names are defaults; override them with the officefloor.transaction.governance.name and
officefloor.transaction.readonly.governance.name properties.
Anything else is defined by a YAML file under officefloor/govern/, the file name being the
governance name used in govern::
# File: officefloor/govern/audit.yml
governance:
class: net.officefloor.tutorial.springrestgovernance.AuditGovernanceGuard an endpoint with a Spring Security SpEL expression, evaluated before the first function runs.
Place it in a composition: block, which applies it to the whole file:
# officefloor/rest/security/yaml.GET.yml
composition:
authorize: "hasRole('ADMIN')"
service:
class: net.officefloor.tutorial.springrestsecurity.YamlAuthorizeServiceThis is the OfficeFloor-native alternative to @PreAuthorize on a controller method. The expression
is parsed at start-up, so a syntax error fails the build rather than the request.
A YAML file named without a .METHOD part is a path config file rather than an endpoint. Its
top-level authorize: is inherited by every endpoint at and below that path:
# File: officefloor/rest/security/admin.yml → guards everything under /security/admin
authorize: "hasRole('ADMIN')"Resolution takes the endpoint's own composition.authorize first, then walks up the parent path
chain — the most specific (deepest) expression wins. So a single file secures a whole subtree, and an
individual endpoint can override it. An empty expression opens a path back up:
# File: officefloor/rest/security/admin/open.GET.yml → public, despite the parent path config
composition:
authorize: ""Alongside officefloor/rest/, endpoints can draw on:
officefloor/escalation/— global exception handlers, named by exception classofficefloor/govern/— governance definitions referenced bygovern:officefloor/managedobjects/— custom managed object state sources
# src/main/resources/officefloor/rest/greeting.POST.yml → POST /greeting
validate:
class: ValidateGreetingLogic
outputs:
valid: build
build:
class: PostGreetingLogic
next: audit
audit:
class: AuditGreetingLogicEach function class declares only its own Spring bean dependencies, injected by Spring exactly as they would be in any other bean. No function knows about the others. The YAML file is the complete specification of the endpoint.
See Spring Integration for how the handler methods obtain Spring beans and use Spring MVC parameter annotations.
See the REST CRUD Orchestration tutorial for these keys applied to a full resource, and the Orchestration Patterns and Naming reference for the function naming conventions and the request to response data flow.