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.
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.