Variable Tutorial
This tutorial demonstrates passing values downstream between functions using variables within a Spring Boot application.
Unlike direct method parameters, variables do not need to be passed explicitly by the caller. A variable set in one function is available for injection into any downstream function, allowing pipelines to be composed from independent classes that know nothing of each other.
Maven dependency
<dependency>
<groupId>net.officefloor.springboot</groupId>
<artifactId>officefloor-rest-spring-boot-4-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Application class
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Out<T> / Var<T>
The following method demonstrates setting values for variables.
public class OutLogic {
public static void setValues(Out<Person> person, @Description Out<String> description) {
person.set(new Person("Daniel", "Sagenschneider"));
description.set("Need to watch his code!");
}
}
Variables are created by reflectively interrogating the method parameters. Because many variables may share the same type, qualifiers distinguish them — much like naming a variable.
The @Qualifier meta-annotation is the recommended approach. The qualifier used in this tutorial is:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Description {
}
Prefer Out<T> to set a variable. This allows the compiler to reason about out/in combinations and catch uninitialised variables.
When a function must both read and write the same variable, use Var<T>. Use this sparingly as it reduces the ability to statically reason about flow composition.
public class VarLogic {
public static void setValues(Var<Person> person, @Description Var<String> description) {
person.set(new Person("Daniel", "Sagenschneider"));
description.set("Need to watch his code!");
}
}
@Val / In<T>
The following method demonstrates reading variable values downstream.
public class ValLogic {
public static void useValues(@Val Person person, @Description @Val String description,
ObjectResponse<ServerResponse> response) {
response.send(new ServerResponse(person, description));
}
}
@Val annotates a parameter to receive the current value of a variable. It distinguishes the variable from other injected objects such as ObjectResponse.
The preferred combination is Out<T> upstream with @Val downstream — readable and avoids importing extra framework types at the read site.
For symmetry there is also In<T>:
public class InLogic {
public static void useValues(In<Person> person, @Description In<String> description,
ObjectResponse<ServerResponse> response) {
response.send(new ServerResponse(person.get(), description.get()));
}
}
A shared scope for functions
Variables give a set of functions the same convenience that a single method gives its statements.
A single method has one local scope. Each statement can use any variable declared above it. Set a value on one line and every later line can use it. The state accumulates in scope as the method runs.
Composing the same work from separate functions would normally lose that scope. A function sees only its own parameters. With return values a function hands one value to the next. To carry several accumulated values it has to bundle them into a type. As the flow grows, so does the number of these carrier types, one for each combination of state in flight. The types model nothing. They exist only to move state between functions.
Variables restore the shared scope. A function publishes a value with Out. Any later function reads it with @Val. The set of live variables is the shared scope, so each function reads any earlier value it needs. Each function is like a block of code in the single method, with its variables still available to the functions that follow. There are no carrier types.
It also improves on the single method. In a method a block's use of the surrounding variables is implicit, and the block cannot be reused on its own. A function instead declares the variables it reads and the variables it writes. Its inputs and outputs are explicit, so the function is reusable wherever those variables are in scope.
So variables give the flexibility of a shared local scope with the discipline of an explicit, reusable contract on each function.
Composing flows — YAML endpoint files
The YAML files wire together the upstream setter and downstream reader as a two-function pipeline. Each function is independent — neither class references the other.
OutSet:
class: net.officefloor.tutorial.variablehttpserver.OutLogic
method: setValues
next: InUse
InUse:
class: net.officefloor.tutorial.variablehttpserver.InLogic
method: useValues
VarSet:
class: net.officefloor.tutorial.variablehttpserver.VarLogic
method: setValues
next: ValUse
ValUse:
class: net.officefloor.tutorial.variablehttpserver.ValLogic
method: useValues
OutToValSet:
class: net.officefloor.tutorial.variablehttpserver.OutLogic
method: setValues
next: OutToValUse
OutToValUse:
class: net.officefloor.tutorial.variablehttpserver.ValLogic
method: useValues
Variables in a REST pipeline
Two rules make variables the natural way to pass request state between functions.
First, the HTTP request body can be read only once. Only one function may bind @RequestBody. When a later function also needs the body, the first function binds it once and publishes it with Out. The later function reads the variable, not the body.
Second, @Val returns the same object the producer stored. It is not a copy. So one function can load an entity into a variable, a second function can mutate that entity in place, and a third function sees the change.
The REST CRUD Orchestration tutorial uses both rules to build a full resource. The Orchestration Patterns and Naming reference states them in full.
Testing
Tests use MockMvc to exercise the full composed pipeline through Spring MVC:
@SpringBootTest
@AutoConfigureMockMvc
public class VariableHttpServerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper mapper;
private static final ServerResponse EXPECTED =
new ServerResponse(new Person("Daniel", "Sagenschneider"), "Need to watch his code!");
@Test
public void outIn() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/outIn")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(EXPECTED)));
}
@Test
public void varVal() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/varVal")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(EXPECTED)));
}
@Test
public void outVal() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/outVal")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(mapper.writeValueAsString(EXPECTED)));
}
}
Next
The Governance tutorial wraps function execution with cross-cutting concerns such as transactions, declared in YAML.

