Architecture
jEAP Server-Sent Events pushes resource-change notifications from a service to its browser clients.
The challenge it solves is horizontal scaling: an EventSource connection is bound to a single
backend instance, but a change may be triggered on any instance. The library therefore routes every
change through a Kafka topic so all instances forward it to their own connected clients.
Modules and responsibilities
| Module | Key types |
|---|---|
core | ResourceMutationService, ResourceMutationType, ResourceMutationEventHandler, listener interfaces |
messaging | NotifyClientCommandProducer, NotifyClientCommandConsumer, contract & topic validators |
web | NotifyClientController (SSE endpoint), NotifyClientResourceMutationDataSender, NotifyClientHeartbeatSender, authorization |
starter | Aggregates the above plus ServerSentEventsAutoConfiguration |
Event flow
A resource mutation travels from business code through Kafka back into every instance's SSE emitters:
Step by step:
- Publish. Business code calls
ResourceMutationService.resourceMutation(type, resourcePath). The service fans out to all registeredResourceMutationListeners; in a configured service the only listener isNotifyClientCommandProducer. - To Kafka. The producer builds a
NotifyClientCommand(Avro) tagged with the sending application name and theresourcePath, and sends it synchronously tojeap.sse.kafka.topic. - Consume on every instance. Each instance runs a
NotifyClientCommandConsumerwith a unique listener id (${spring.application.name}-${random.uuid}), so all instances receive the command. - Filter.
ResourceMutationEventHandlerdrops commands whosesendingApplicationdiffers from the instance's ownspring.application.name— only an application notifies its own clients. - Push.
NotifyClientResourceMutationDataSender(aResourceMutationEventListener) serializes{"path": resourcePath}to JSON and callsNotifyClientController.sendEvent(type, data), which writes the event to every activeSseEmitter.
Multi-instance support
A service usually runs as several instances behind a load balancer. Each browser client holds its
EventSource connection to exactly one of them, while the mutation it should be notified about may
be processed on any other. The diagram below shows how the Kafka topic bridges this gap: UI 1 is
connected to instance 1, UI 2 to instance 2, and a resource deletion handled by instance 2 still
reaches both clients.
Every instance subscribes to the topic with its own consumer group (the listener id is
${spring.application.name}-${random.uuid}), so the NotifyClientCommand is not load-balanced
across instances but delivered to all of them. Each instance then forwards the event to the
clients connected to it — no instance needs to know where the other clients are attached. Because
the consumer group id is regenerated on every start, no offsets are carried over between restarts:
the notifications are volatile by design and only relevant to clients connected at that moment.
As with any jEAP messaging participant, the service registers producer and consumer
message contracts for the topic with the jEAP Message Contract Service.
The SSE endpoint
NotifyClientController exposes GET ${jeap.sse.web.endpoint} producing text/event-stream. Each
subscribing client gets its own SseEmitter (timeout jeap.sse.web.emitter.timeoutInMs) added to a
CopyOnWriteArrayList; emitters are removed on completion, timeout or error. A separate
NotifyClientHeartbeatSender pushes a HEARTBEAT event at jeap.sse.web.heartbeat.rateInMs to keep
intermediaries from closing idle connections.
Why only references travel
By design an SSE event carries only an event type and a resourcePath reference, never the full
resource data. SSE streams cannot be covered by consumer-driven contract tests, so the client uses
the reference to fetch the current data with a normal, testable REST call.