Skip to main content

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

ModuleKey types
coreResourceMutationService, ResourceMutationType, ResourceMutationEventHandler, listener interfaces
messagingNotifyClientCommandProducer, NotifyClientCommandConsumer, contract & topic validators
webNotifyClientController (SSE endpoint), NotifyClientResourceMutationDataSender, NotifyClientHeartbeatSender, authorization
starterAggregates 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:

  1. Publish. Business code calls ResourceMutationService.resourceMutation(type, resourcePath). The service fans out to all registered ResourceMutationListeners; in a configured service the only listener is NotifyClientCommandProducer.
  2. To Kafka. The producer builds a NotifyClientCommand (Avro) tagged with the sending application name and the resourcePath, and sends it synchronously to jeap.sse.kafka.topic.
  3. Consume on every instance. Each instance runs a NotifyClientCommandConsumer with a unique listener id (${spring.application.name}-${random.uuid}), so all instances receive the command.
  4. Filter. ResourceMutationEventHandler drops commands whose sendingApplication differs from the instance's own spring.application.name — only an application notifies its own clients.
  5. Push. NotifyClientResourceMutationDataSender (a ResourceMutationEventListener) serializes {"path": resourcePath} to JSON and calls NotifyClientController.sendEvent(type, data), which writes the event to every active SseEmitter.

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.