SupplierSource in OfficeFloor

The previous tutorial showed how a single ManagedObjectSource registers one managed object type. This tutorial introduces SupplierSource: a single YAML declaration that registers an entire library of related managed object types from one source.

The distinction matters in practice:

  • A Spring bean or individual managed object is always declared independently. If two objects share internal state (a connection pool, a queue, a DI container) there is no clean place to put that shared state — it ends up as a Spring singleton that both beans reference, coupling unrelated configuration.
  • A SupplierSource owns the shared state. It registers all the types that expose that state through their own managed object wrappers. Callers simply inject whichever type they need; the supplier ensures they all see the same underlying resource.

Typical use cases:

  • Integrating a third-party messaging or streaming library (e.g. Kafka, JMS) that naturally provides producer and consumer as a matched pair sharing a connection.
  • Bridging another dependency injection framework (e.g. Guice, CDI) — the supplier wraps that framework's injector and vends its objects as OfficeFloor managed objects.
  • Providing a coherent client set for an external service — e.g. a REST client, its retry policy, and its circuit breaker — all wired to the same underlying HTTP connection pool held by the supplier.

The tutorial models an in-memory messaging service. One SupplierSource registers both MessagePublisher and MessageSubscriber, both sharing the same internal queue. The tutorial exposes two endpoints: POST /message to publish and GET /message to receive.

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

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

The domain types

MessagePublisher and MessageSubscriber are plain Java classes. Their constructors are package-private: only SupplierSource should create them, ensuring every instance is backed by the same shared queue.

public class MessagePublisher {

	private final LinkedBlockingDeque<String> queue;

	MessagePublisher(LinkedBlockingDeque<String> queue) {
		this.queue = queue;
	}

	public void publish(String content) {
		queue.addLast(content);
	}
}
public class MessageSubscriber {

	private final LinkedBlockingDeque<String> queue;

	MessageSubscriber(LinkedBlockingDeque<String> queue) {
		this.queue = queue;
	}

	public String receive() {
		return queue.pollFirst();
	}
}

The SupplierSource — MessagingSupplierSource

AbstractSupplierSource is extended and SupplierSourceContext.addManagedObjectSource() is called once per type inside SupplierSource.supply().

The shared messageQueue field lives on the supplier instance. Both inline AbstractManagedObjectSource implementations capture it via closure, so every MessagePublisher and every MessageSubscriber created across all requests write to and read from exactly the same deque. None is used for both generic parameters because neither managed object source requires OfficeFloor-managed dependencies or declares flow outputs:

public class MessagingSupplierSource extends AbstractSupplierSource {

	private final LinkedBlockingDeque<String> messageQueue = new LinkedBlockingDeque<>();

	@Override
	protected void loadSpecification(SpecificationContext context) {
	}

	@Override
	public void supply(SupplierSourceContext context) throws Exception {

		// Register MessagePublisher — captures the shared queue via closure
		context.addManagedObjectSource(null, MessagePublisher.class,
				new AbstractManagedObjectSource<None, None>() {
					@Override
					protected void loadSpecification(SpecificationContext context) {}

					@Override
					protected void loadMetaData(MetaDataContext<None, None> context) throws Exception {
						context.setObjectClass(MessagePublisher.class);
					}

					@Override
					protected ManagedObject getManagedObject() throws Throwable {
						return () -> new MessagePublisher(messageQueue);
					}
				});

		// Register MessageSubscriber — captures the same shared queue via closure
		context.addManagedObjectSource(null, MessageSubscriber.class,
				new AbstractManagedObjectSource<None, None>() {
					@Override
					protected void loadSpecification(SpecificationContext context) {}

					@Override
					protected void loadMetaData(MetaDataContext<None, None> context) throws Exception {
						context.setObjectClass(MessageSubscriber.class);
					}

					@Override
					protected ManagedObject getManagedObject() throws Throwable {
						return () -> new MessageSubscriber(messageQueue);
					}
				});
	}

	@Override
	public void terminate() {
		messageQueue.clear();
	}
}

The terminate() method is called when OfficeFloor shuts down, giving the supplier the opportunity to release any resources it holds.

Configuring the supplier — officefloor/suppliers/

Suppliers are declared as YAML files in src/main/resources/officefloor/suppliers/. The source field names the SupplierSource implementation. An optional properties: map is passed to SupplierSource.supply() via the SupplierSourceContext:

source: net.officefloor.tutorial.springrestsupplier.MessagingSupplierSource

One YAML file registers all types that the supplier provides. Drop any number of YAML files into officefloor/suppliers/; each file is one supplier.

Service methods

MessagePublisher and MessageSubscriber are accepted by type — no qualifier, no explicit wiring required. OfficeFloor finds the supplier that registered each type and injects the managed object. The HTTP request body is bound with @RequestBody:

public class PublishService {

	public void service(@RequestBody Message message, MessagePublisher publisher) {
		publisher.publish(message.getContent());
	}
}
public class SubscribeService {

	public void service(MessageSubscriber subscriber, ObjectResponse<String> response) {
		String content = subscriber.receive();
		response.send(content != null ? content : "");
	}
}

The request body for POST /message:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Message {

	private String content;
}

REST endpoints

service:
  class: net.officefloor.tutorial.springrestsupplier.PublishService
service:
  class: net.officefloor.tutorial.springrestsupplier.SubscribeService

Testing

@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestSupplierHttpServerTest {

	@Autowired
	private MockMvc mvc;

	@Autowired
	private ObjectMapper mapper;

	@Test
	public void publishedMessageIsReceivedBySubscriber() throws Exception {
		// Publish returns 204 (No Content) — correct for a fire-and-forget operation
		mvc.perform(post("/message")
				.contentType(MediaType.APPLICATION_JSON)
				.content(mapper.writeValueAsString(new Message("hello from supplier"))))
				.andExpect(status().is2xxSuccessful());

		// Subscriber reads from the same shared queue held by the supplier
		mvc.perform(get("/message"))
				.andExpect(status().isOk())
				.andExpect(content().string("hello from supplier"));
	}

	@Test
	public void multipleMessagesAreQueuedInOrder() throws Exception {
		mvc.perform(post("/message")
				.contentType(MediaType.APPLICATION_JSON)
				.content(mapper.writeValueAsString(new Message("first"))))
				.andExpect(status().is2xxSuccessful());
		mvc.perform(post("/message")
				.contentType(MediaType.APPLICATION_JSON)
				.content(mapper.writeValueAsString(new Message("second"))))
				.andExpect(status().is2xxSuccessful());

		// Queue preserves FIFO insertion order across requests
		mvc.perform(get("/message"))
				.andExpect(status().isOk())
				.andExpect(content().string("first"));
		mvc.perform(get("/message"))
				.andExpect(status().isOk())
				.andExpect(content().string("second"));

		// Queue is now drained — next receive returns empty
		mvc.perform(get("/message"))
				.andExpect(status().isOk())
				.andExpect(content().string(""));
	}
}

publishedMessageIsReceivedBySubscriber proves the end-to-end path: a message published in one request is visible to the subscriber in the next. This works because both the publisher and subscriber write to and read from the same deque held by the supplier — not because of any Spring bean or shared static state.

multipleMessagesAreQueuedInOrder confirms that the queue preserves insertion order: two successive publishes followed by two successive receives return messages in FIFO order.

receiveReturnsEmptyWhenQueueIsEmpty covers the empty-queue path.

Note that because the supplier's queue persists for the application lifetime, test order matters: each test should leave the queue empty. publishedMessageIsReceivedBySubscriber and multipleMessagesAreQueuedInOrder each drain every message they publish, so no leakage occurs between tests.

Supplier vs managed object vs Spring bean

Mechanism Registers …
Spring @Bean One bean per method; shared state requires a separate singleton bean referenced by each other bean.
Managed object YAML One managed object per YAML file; no built-in way to share state between two separate files.
Supplier YAML Any number of managed object types from one YAML file; the supplier instance holds shared state that all its types access via closure.

What you have now

After completing this tutorial you can:

  • Understand that SupplierSource exists to register a library of related objects from a single YAML declaration, with the supplier instance holding any shared state
  • Extend AbstractSupplierSource and call SupplierSourceContext.addManagedObjectSource() once per type inside supply()
  • Place a YAML file in officefloor/suppliers/ with a source: field to activate the supplier, optionally passing properties: for configuration
  • Inject any type the supplier registers into service methods by type, alongside Spring beans and individually declared managed objects, with no additional annotation
  • Implement terminate() to release resources when OfficeFloor shuts down

Next

The Spring REST Governance tutorial demonstrates how OfficeFloor Governance wraps function execution with framework-managed pre/post lifecycle via YAML configuration — without annotations on the service class.