Governance in OfficeFloor

Governance is OfficeFloor's mechanism for wrapping function execution with framework-managed pre/post lifecycle. Where the previous tutorials introduced managed objects (per-request state) and suppliers (libraries of related objects), governance answers the question: who ensures the correct lifecycle is applied to those objects, consistently, without relying on the caller to remember?

Spring addresses this with AOP proxies (@Transactional, @PreAuthorize, etc.). OfficeFloor's governance is conceptually similar but operates at the framework execution level rather than the proxy level:

  • Governance is declared in a YAML file in officefloor/govern/, named, and applied to individual REST functions via govern: [ name ] in their YAML configuration. No annotation on the service class is required.
  • The governed objects are managed objects that implement a typed extension interface. OfficeFloor detects eligibility at compile time and enrolls them automatically.
  • The governance lifecycle — begin, enforce (commit), disregard (rollback) — is managed entirely by the OfficeFloor runtime, not by a proxy that must intercept every call path.

The tutorial demonstrates this by attaching an audit governance to one REST endpoint and leaving the other ungoverned. Both share the same AuditRecord managed object. The response event list shows exactly when governance fires relative to the function body.

Tutorial Source

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 SpringRestGovernanceApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringRestGovernanceApplication.class, args);
	}
}

The governance extension interface — Auditable

Each governance infers its extension type from the parameter of the method annotated with @Govern. Any managed object implementing that type in a governed function is enrolled automatically via OfficeGovernance.enableAutoWireExtensions().

public interface Auditable {

	void recordEvent(String event);
}

The governed managed object — AuditRecord

AuditRecord is a PROCESS-scoped managed object declared in officefloor/managedobjects/. It implements Auditable, which is what makes it eligible for governance enrollment. Because the scope is PROCESS, each request gets a fresh instance with an empty event list — the perfect scratchpad for tracking what happened within that request.

public class AuditRecord implements Auditable {

	private final List<String> events = new ArrayList<>();

	@Override
	public void recordEvent(String event) {
		events.add(event);
	}

	public List<String> getEvents() {
		return new ArrayList<>(events);
	}
}
managed-object:
  class: net.officefloor.tutorial.springrestgovernance.AuditRecord
  scope: PROCESS

The governance — AuditGovernance

A class-based governance uses three annotations from the net.officefloor.plugin.governance.clazz package:

  • @Govern — called by OfficeFloor before the function body executes, once per enrolled managed object. The parameter type declares the extension interface. Analogous to beginning a transaction.
  • @Enforce — called by OfficeFloor after the function body completes successfully. Analogous to committing a transaction.
  • @Disregard — called by OfficeFloor when the function throws an unhandled exception. Analogous to rolling back a transaction.

A new instance of the governance class is created per function invocation, so storing the enrolled object as a field is safe and shared across all three lifecycle methods:

public class AuditGovernance {

	private Auditable enrolled;

	@Govern
	public void govern(Auditable auditable) {
		this.enrolled = auditable;
		auditable.recordEvent("governance-begin");
	}

	@Enforce
	public void enforce() {
		enrolled.recordEvent("governance-commit");
	}

	@Disregard
	public void disregard() {
		enrolled.recordEvent("governance-rollback");
	}
}

For more complex governance — declaring flow outputs, injecting dependencies, or performing asynchronous setup — extend AbstractGovernanceSource and reference the class with source: in the YAML instead of class:.

Configuring governance — officefloor/govern/

Governance is declared as YAML files in src/main/resources/officefloor/govern/. The file name becomes the governance name used in govern: lists. The class field names the governance implementation:

governance:
  class: net.officefloor.tutorial.springrestgovernance.AuditGovernance

Any number of YAML files may be placed in officefloor/govern/; each declares one governance. A source: field accepts a custom AbstractGovernanceSource implementation for cases that require flow outputs or properties:

governance:
  source: com.example.MyGovernanceSource
  properties:
    timeout.ms: 5000

Service methods

The governed service carries govern: [ audit ] in its REST YAML. No annotation is needed on the service class itself:

public class GovernedService {

	public void service(AuditRecord record, ObjectResponse<List<String>> response) {
		record.recordEvent("service-called");
		response.send(record.getEvents());
	}
}
service:
  class: net.officefloor.tutorial.springrestgovernance.GovernedService
  govern: [ audit ]

The ungoverned service is identical in code but carries no govern: declaration. AuditGovernance is never activated for this function even though AuditRecord implements Auditable:

public class UngovernedService {

	public void service(AuditRecord record, ObjectResponse<List<String>> response) {
		record.recordEvent("service-called");
		response.send(record.getEvents());
	}
}
service:
  class: net.officefloor.tutorial.springrestgovernance.UngovernedService

Testing

@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestGovernanceHttpServerTest {

	@Autowired
	private MockMvc mvc;

	@Autowired
	private ObjectMapper mapper;

	@Test
	public void governedEndpointRunsGovernanceBeforeServiceBody() throws Exception {
		MvcResult result = mvc.perform(get("/governed").accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andReturn();

		List<String> events = mapper.readValue(
				result.getResponse().getContentAsString(),
				new TypeReference<List<String>>() {});

		assertTrue(events.contains("governance-begin"),
				"Governance must call @Govern before function body runs");
		assertTrue(events.contains("service-called"),
				"Service body must execute");

		// Governance fires BEFORE the service method body
		assertTrue(events.indexOf("governance-begin") < events.indexOf("service-called"),
				"@Govern must precede the service method body");
	}

	@Test
	public void ungovernedEndpointSkipsGovernance() throws Exception {
		MvcResult result = mvc.perform(get("/ungoverned").accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andReturn();

		List<String> events = mapper.readValue(
				result.getResponse().getContentAsString(),
				new TypeReference<List<String>>() {});

		assertFalse(events.contains("governance-begin"),
				"Governance must not run on a function with no govern: declaration");
		assertTrue(events.contains("service-called"),
				"Service body must still execute without governance");
	}
}

governedEndpointRunsGovernanceBeforeServiceBody reads the event list from the response and asserts three things: "governance-begin" is present (proving @Govern ran), "service-called" is present (proving the service body ran), and "governance-begin" precedes "service-called" in the list (proving @Govern fires before the function body).

Note that "governance-commit" does not appear in the response: the HTTP response is sent during the service method body, and @Enforce runs after the method returns. This is correct behaviour — enforcement is a post-response cleanup, exactly as a database commit happens after the application logic completes.

ungovernedEndpointSkipsGovernance confirms that a function without a govern: declaration never activates the governance even when its managed objects implement the extension interface.

Governance vs Spring AOP

Mechanism How it wraps a function
Spring @Transactional (AOP) A proxy intercepts calls to the annotated method. The annotation must be on a Spring-managed bean. Self-invocation (calling the method from within the same class) bypasses the proxy and skips the transaction.
OfficeFloor Governance The OfficeFloor runtime wraps the function at the framework level, before the method is invoked. No proxy is involved. Any function in any YAML pipeline can be governed regardless of whether its class is a Spring bean, and self-invocation is not a concept in a pipeline model.

Governance is particularly useful when the governed logic spans multiple pipeline steps, when the governed object is a managed object rather than a Spring bean, or when you want the governance declaration to live in the YAML alongside the pipeline it wraps rather than scattered across Java annotations.

What you have now

After completing this tutorial you can:

  • Declare a class-based governance in officefloor/govern/ using @Govern, @Enforce, and @Disregard to implement begin, commit, and rollback lifecycle
  • Define a governance extension interface and implement it on managed objects to make them automatically eligible for enrollment
  • Apply the governance to individual REST functions via govern: [ name ] in the REST YAML — no annotation required on the service class
  • Understand that @Enforce runs after the HTTP response is sent, making it suitable for cleanup and commit rather than for populating the response
  • Extend AbstractGovernanceSource for governances that need flow outputs, dependencies, or YAML-injected properties

Next

The Variables tutorial demonstrates passing state downstream through a procedure flow without coupling callers to callees using Out<T> and @Val.