Bloom is a lightweight, pure-Java runtime event bus for internal application events.
It is part of the Flower JVM ecosystem, but it is not Flower itself. Flower owns workflow structure and execution. Bloom only carries in-process events between objects that should not know about each other directly.
Bloom is small on purpose: it gives you typed publish/subscribe, explicit subscription handles, predictable dispatch rules, and optional Spring integration without making Spring the owner of the event model.
The important word is runtime. A Bloom bus is a normal Java object: create one, pass it to a component, scope it to a feature, wrap it with an async executor, replace it in a test, or let a workflow step subscribe for only the time it is active.
- Publishing domain events inside one JVM.
- Decoupling modules without introducing a message broker.
- Creating event buses at runtime for a component, workflow, test, or adapter.
- Dynamically subscribing and unsubscribing handlers while objects are alive.
- Building framework adapters that need a minimal event bus SPI.
- Keeping tests deterministic with a local in-memory bus.
- Wiring Spring bean methods with
@Subscribewhen the application happens to run inside Spring.
Bloom is not a distributed event system, persistent queue, retry engine, or transaction manager. If an event must survive process failure or cross service boundaries, use a real messaging system and adapt it separately.
Bloom is small infrastructure, not the main runtime.
In Flower JVM, the rough split is:
Flower
owns workflow structure, Flow / Step state, ticking, checkpoint, recovery,
and lifecycle.
Bloom
carries in-memory runtime notifications between objects that should not know
about each other directly.
Flower does not need Bloom to exist. flower-core has its own minimal
EventBus SPI and default in-memory implementation. Bloom becomes useful when
an application wants a slightly more general runtime event bus that can also be
used outside Flower, or when a Flower runtime wants to share events with other
application components through the optional bloom-flower-adapter.
Typical Flower-related uses:
external callback -> publish event -> waiting Flower step receives signal
approval decision -> publish event -> approval step continues
tool/model result -> publish event -> workflow advances
domain event -> publish event -> projection/listener updates
Bloom should stay boring. It is the in-memory event pipe. It should not become the workflow engine, policy engine, AI harness, message broker, or durable audit log.
- Flower runtime: https://github.com/flowerjvm/flower
- Bloom event bus: https://github.com/flowerjvm/bloom
- AI Harness: https://github.com/flowerjvm/flower-ai-harness
- Samples: https://github.com/flowerjvm/flower-sample
Bloom is not an AI framework.
Its role in AI-enabled applications is ordinary but useful: AI and agent systems produce many asynchronous callbacks and internal notifications. Bloom can carry those notifications inside one JVM while keeping publishers and listeners loosely coupled.
Examples:
LLM response received
MCP tool call completed
human approval approved/rejected
agent action proposal created
workflow state changed
Bloom only transports these events in memory. Higher-level modules should own the important decisions:
flower-ai-harness
owns reliable AI call lifecycle.
flower-agent-runtime
owns action policy, approval, audit, and controlled execution.
Flower
owns workflow execution.
Bloom
owns lightweight in-process notification delivery.
Spring already has application events, and they are useful when the event bus is
part of the Spring ApplicationContext. Bloom exists for a different center of
gravity: runtime-scoped internal events.
Use Spring events when:
- the publisher and listener are Spring-managed beans,
- the event is naturally application-context-wide,
- lifecycle and listener discovery should be owned by Spring,
- you are happy to depend on Spring infrastructure in that layer.
Use Bloom when:
- the code should run without Spring,
- an event bus needs to be created per runtime object, test, workflow, or module,
- subscriptions should be explicit handles that can be closed immediately,
- a component should decide exactly when it starts and stops listening,
- the same event SPI should work in core Java, Spring, and framework adapters.
In other words, Spring events are application-context events. Bloom events are runtime object events. Bloom can be used from Spring, but it does not require Spring to define the event boundary.
bloom-core: dependency-free event bus API and implementations.bloom-spring: Spring Framework integration with@EnableBloomand@Subscribe.bloom-flower-adapter: optional Flower integration that exposes a BloomEventBusas Flower'sEventBusSPI.
The Flower adapter is part of the default Maven reactor and resolves Flower
0.1.1 from Maven Central:
mvn -pl bloom-flower-adapter -am verifyEventBus is the main API:
EventBus bus = LocalEventBus.create();
Subscription subscription = bus.subscribe(OrderPlaced.class, event -> {
System.out.println("order placed: " + event.orderId());
});
bus.publish(new OrderPlaced("order-1"));
subscription.close();Events are plain Java objects. They do not need to extend a base class or implement a marker interface.
Bloom dispatches by exact runtime class:
bus.subscribe(ParentEvent.class, event -> handleParent(event));
bus.subscribe(ChildEvent.class, event -> handleChild(event));
bus.publish(new ChildEvent());Only the ChildEvent handler receives the event above. A handler registered for
ParentEvent.class does not receive subclasses. This keeps dispatch predictable
and avoids accidental broad subscriptions.
subscribe(...) returns a Subscription. Keep it if the listener has a shorter
lifetime than the bus:
Subscription sub = bus.subscribe(CacheInvalidated.class, this::invalidate);
// later
sub.close();Closing a subscription is idempotent.
LocalEventBus isolates handler failures. One failing handler does not prevent
other handlers from receiving the same event.
LocalEventBus bus = LocalEventBus.create();
bus.onListenerError((event, handler, cause) -> {
// log, count, or surface listener failures here
});If no error handler is installed, listener failures are ignored.
LocalEventBus dispatches on the publishing thread:
LocalEventBus bus = LocalEventBus.create();
bus.subscribe(UserRegistered.class, event -> sendWelcomeMail(event.userId()));
bus.publish(new UserRegistered("u-1"));Use this when you want deterministic behavior and simple tests.
AsyncEventBus wraps another bus and schedules publish(...) on an executor.
Bloom does not own or shut down the executor; the caller owns its lifecycle.
ExecutorService executor = Executors.newFixedThreadPool(4);
EventBus bus = new AsyncEventBus(LocalEventBus.create(), executor);
bus.subscribe(OrderPaid.class, event -> reserveInventory(event.orderId()));
bus.publish(new OrderPaid("order-1"));
executor.shutdown();Subscriptions still belong to the delegate bus. Only publishing is scheduled asynchronously.
Bloom handlers can be written as lambdas for small cases, but you can also make event handling explicit with classes. This is useful when a handler owns dependencies, has a lifecycle, or should be easy to find from an IDE or by an AI coding assistant.
EventHandler<E> is a functional interface, so a handler class only needs one
method:
public final class SendWelcomeMailHandler implements EventHandler<UserRegistered> {
private final MailService mailService;
public SendWelcomeMailHandler(MailService mailService) {
this.mailService = mailService;
}
@Override
public void handle(UserRegistered event) {
mailService.sendWelcomeMail(event.userId());
}
}
EventBus bus = LocalEventBus.create();
SendWelcomeMailHandler handler = new SendWelcomeMailHandler(mailService);
Subscription sub = bus.subscribe(UserRegistered.class, handler);
bus.publish(new UserRegistered("u-1"));Use this style when the subscription is owned by application setup code.
Use AbstractTypedEventHandler<E> when the handler object should know its own
event type and manage its own subscription:
public final class InventoryReservedHandler
extends AbstractTypedEventHandler<InventoryReserved> {
private final InventoryProjection projection;
public InventoryReservedHandler(InventoryProjection projection) {
super(InventoryReserved.class);
this.projection = projection;
}
@Override
protected void onEvent(InventoryReserved event) {
projection.markInventoryReserved(event.orderId());
}
}
InventoryReservedHandler handler = new InventoryReservedHandler(projection);
handler.subscribeTo(bus);
// later, when this handler is no longer needed
handler.close();This keeps the "one handler handles one event type" rule visible in the class itself.
Use AbstractEventSubscriber when one object owns several subscriptions:
public final class OrderProjection extends AbstractEventSubscriber {
private final OrderReadModel readModel;
public OrderProjection(EventBus bus, OrderReadModel readModel) {
this.readModel = readModel;
on(bus, OrderPlaced.class, this::onOrderPlaced);
on(bus, OrderPaid.class, this::onOrderPaid);
on(bus, OrderCancelled.class, this::onOrderCancelled);
}
private void onOrderPlaced(OrderPlaced event) {
readModel.create(event.orderId());
}
private void onOrderPaid(OrderPaid event) {
readModel.markPaid(event.orderId());
}
private void onOrderCancelled(OrderCancelled event) {
readModel.markCancelled(event.orderId());
}
}Calling close() releases every tracked subscription:
OrderProjection projection = new OrderProjection(bus, readModel);
// later
projection.close();Use this style for read models, in-memory projections, adapters, or components that naturally listen to a small group of related events.
For very small examples, lambdas are still fine:
bus.subscribe(OrderPlaced.class, event -> readModel.create(event.orderId()));For production code, prefer named handler classes when that makes ownership, dependencies, and lifecycle clearer.
bloom-spring does not turn Bloom into Spring application events. It simply
registers Spring bean methods as handlers on a Bloom EventBus.
Add bloom-spring, enable Bloom, and annotate bean methods:
@Configuration
@EnableBloom
class AppConfig {
}
@Component
class OrderEventHandlers {
@Subscribe
public void on(OrderPlaced event) {
// method must have exactly one parameter
}
}@EnableBloom imports a default EventBus if none exists and registers the bean
post-processor that scans @Subscribe methods.
You can also provide your own EventBus bean if you want a specific runtime
scope or an async wrapper.
- Keep event classes small and immutable.
- Treat Bloom events as in-memory notifications, not durable facts.
- Decide the bus scope deliberately: global JVM bus, feature-local bus, workflow-local bus, test-local bus, or Spring bean.
- Subscribe to concrete event classes; dispatch is exact-type by design.
- Keep handler work short when using
LocalEventBus; long work should move to an executor or a service boundary. - Always close subscriptions owned by temporary objects.
- Prefer one event type per handler method or handler class.
- Install an error handler in production if listener failures should be visible.
Artifacts are not published to Maven Central yet. Until then, install Bloom locally before building projects that depend on it:
mvn test
mvn installApache License 2.0.