Sunday, 19 July 2026

PetClinic Converted Again, This Time All The Way To Functions

Last month I wrote about moving OfficeFloor v4 from one graphical file to YAML per endpoint. Spring PetClinic REST was the worked example. The configuration story was right. The code story was not.

That conversion gave each endpoint its own YAML file. But behind most of those files sat a single function. It did everything the old controller method did. Validate. Look up. Mutate. Save. Respond. I had swapped the configuration format and left the fat method alone. Explicit orchestration over one function is just a verbose @GetMapping.

So I did the conversion again. Properly. The code is up as a pull request with the following walk through:

What changed

The controllers are gone. All nine of them. So are ExceptionControllerAdvice and BindingErrorsResponse. In their place are 37 endpoint YAML files and 73 small function classes.

An update endpoint now reads as the steps it always was.

# rest/api/owners/{ownerId}.PUT.yml
composition:
  authorize: "hasRole('OWNER_ADMIN')"

validate:
  class: ...rest.function.owner.ValidateOwner
  govern: [ transaction ]
  next: load

load:
  class: ...rest.function.owner.LoadOwner
  govern: [ transaction ]
  next: apply

apply:
  class: ...rest.function.owner.ApplyOwner
  govern: [ transaction ]
  next: save

save:
  class: ...rest.function.owner.SaveOwner
  govern: [ transaction ]
  next: respond

respond:
  class: ...rest.function.owner.RespondWithOwnerUpdated
  govern: [ transaction ]

Validate. Load. Apply. Save. Respond. You do not reconstruct the flow by reading a method body.

The functions stay small enough to hold in your head.

public class LoadOwner {
    public void service(@PathVariable(name = "ownerId") Integer ownerId,
            OwnerRepository ownerRepository, Out<Owner> loaded) throws NotFoundException {
        loaded.set(Lookups.findOrNotFound(() -> ownerRepository.findById(ownerId),
                "Owner not found: " + ownerId));
    }
}

State moves between steps as variables. Out<Owner> here. @Val Owner in the function that consumes it. The steps stay decoupled. Each one is unit testable on its own.

The parts that stopped being code

Two cross cutting concerns fell out of the Java entirely.

Transactions are govern: [ transaction ] on the steps that need them. The transaction spans the orchestration. Not a method boundary. That is what you wanted from @Transactional anyway.

Exception handling is a directory. The file escalation/...NotFoundException.yml names the handler for NotFoundException. An escalation and its handler are now discoverable by file name.

Security stayed Spring's. The authorize: "hasRole('OWNER_ADMIN')" line is a Spring expression. The repositories, mappers, DTOs and Spring Boot wiring are untouched. This is not a rewrite away from Spring. 

It is Spring with the request flow made explicit.

Was it worth it

There are more files. 73 classes where there were nine controllers. But each one is short. Each is independently testable. Each is named after what it does. And the file that composes them tells you the whole route at a glance.

If you looked at the last conversion and thought the YAML was not buying you much, that was fair. Have another look at this one.

No comments:

Post a Comment