Chapter 09 - Real-Time Updates with Reactive Programming Using Spring WebFlux and Server-Sent Events
This chapter extends the Bookstore microservices with real-time inventory notifications delivered through Server-Sent Events (SSE). The inventory service publishes domain events when books are created or repriced, the gateway exposes those events through a long-lived HTTP stream, and browser or CLI clients consume them without polling.
Use JDK 26 or newer for this chapter. The source is configured with java.version=26, so older JDKs will fail with release version 26 not supported during Maven builds.
The implementation builds directly on the secured gateway from Chapter 08:
- SSE reads are public so dashboards and monitoring pages can subscribe easily.
- Book writes remain authenticated through the gateway and Keycloak-backed JWT validation.
- Observability remains intact because the gateway and services still emit trace and log data for the request path that produced each event.
- What Are Server-Sent Events?
- SSE Architecture in Bookstore
- Implementing SSE in Inventory-MS
- Consuming SSE with Browser and CLI Clients
- Testing and Validating SSE
- Detailed Test Scenarios (API and Tools)
- Event Payload Reference
- Troubleshooting Guide
- Security and Observability for SSE
- Companion Assets
- References
- Summary
Server-Sent Events provide a simple one-way streaming channel from server to client over standard HTTP.
- The server responds with the media type
text/event-stream. - The browser keeps the connection open and listens for named events.
- The client automatically retries if the connection is interrupted.
SSE is a strong fit when the server needs to push notifications, but the client does not need a full duplex protocol such as WebSockets.
Typical use cases include:
- Live notification panels
- Admin dashboards
- Status streams
- Inventory and pricing updates
The Bookstore implementation routes all notification traffic through the gateway and keeps the event production logic inside the inventory service.
Browser / CLI Client
|
| EventSource / curl -N
This chapter source is configured for JDK 26. If you are using an older JDK, Maven will fail with `release version 26 not supported`.
v
API Gateway :8080
|
| /packt/inventory/api/notifications/**
v
Inventory Service :8081
|
| NotificationController
v
NotificationService (Reactor sink)
|
| events emitted by BookService
v
Connected subscribers
Only two business event types are emitted in this chapter:
NEW_BOOKPRICE_CHANGE
The inventory microservice uses Spring WebFlux and Project Reactor to broadcast events.
Core implementation decisions:
- Reactive event broadcasting:
NotificationServiceuses a ReactorSinks.Manyto fan out events to multiple listeners. - Named SSE events:
NotificationControlleremits named SSE frames so clients can subscribe toNEW_BOOKandPRICE_CHANGEindependently. - Heartbeat comments: the stream emits keepalive comments so intermediaries are less likely to time out an otherwise idle connection.
- Gateway-first access: clients subscribe through the gateway path at
/packt/inventory/api/notifications/**, not directly to the service port. - Separate gateway route: the notification route is defined before the general inventory route to avoid circuit breaker behavior interfering with long-lived SSE connections.
The exposed notification endpoints are:
GET /packt/inventory/api/notifications/stream
GET /packt/inventory/api/notifications/stream/filtered?eventType=NEW_BOOK
GET /packt/inventory/api/notifications/status
This chapter includes three client-side validation tools:
test-sse-debug.html: raw event inspector for troubleshooting and payload validationtest-sse.html: reader-friendly event dashboard for book and price notificationstest-sse-curl.sh: terminal subscriber with optional event filtering
All three clients read from the public gateway stream. None of them send authenticated write requests themselves. That separation is intentional:
- Subscription is unauthenticated.
- Book creation and updates require a Bearer token.
This makes the validation flow very clear: keep one client subscribed, then trigger authenticated writes from Postman or curl.
This section is the recommended chapter workflow for proving that the stream, routing, security, and event publication all work together.
By the end of the test, you should have verified all of the following:
- The notification status endpoint is reachable through the gateway.
- At least one client can subscribe successfully to the SSE stream.
- A book creation request emits a
NEW_BOOKevent. - A price patch request emits a
PRICE_CHANGEevent. - The filtered SSE endpoint only emits the requested event type.
Make sure the four chapter services are running:
- Eureka Server
- Inventory Microservice
- User Microservice
- Gateway Server
The gateway entry point used throughout the rest of this chapter is:
http://localhost:8080
Before opening a stream, confirm the gateway can reach the notification endpoint:
curl http://localhost:8080/packt/inventory/api/notifications/statusExpected shape:
{
"status": "UP",
"activeSubscribers": 0,
"message": "Notification service is operating normally"
}The exact subscriber count will vary. Once a browser page or terminal client connects, the count should increase.
Use one of the included chapter tools.
Open test-sse-debug.html, confirm the gateway URL, and click Connect.
Use this page when you want to validate:
- Raw JSON payloads
- Event IDs
- Reconnect behavior
- Subscriber count from the status endpoint
Open test-sse.html and click Connect Stream.
Use this page when you want to validate:
- Event totals
NEW_BOOKandPRICE_CHANGEcounters- Filtered display tabs
- Active subscriber count alongside the event list
./test-sse-curl.shTo point the script to a non-default gateway URL:
SSE_GATEWAY_URL=http://localhost:8080 ./test-sse-curl.shTo validate a filtered stream from the terminal:
./test-sse-curl.sh NEW_BOOK
./test-sse-curl.sh PRICE_CHANGESSE subscriptions are public, but writes still require authentication. Sign in through the gateway and capture the accessToken field from the response.
Example request shape:
curl -X POST http://localhost:8080/packt/user/api/users/signin \
-H "Content-Type: application/json" \
-d '{
"email": "<your-email>",
"password": "<your-password>"
}'After sign-in, export the token in your shell or Postman environment:
export ACCESS_TOKEN="<paste-access-token-here>"The current book creation API expects authorId. That means the validation flow must use an existing author or create one first.
List authors:
curl http://localhost:8080/packt/inventory/api/authors \
-H "Authorization: Bearer ${ACCESS_TOKEN}"If needed, create an author:
curl -X POST http://localhost:8080/packt/inventory/api/authors \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Josh Long",
"nationality": "American"
}'Save the returned author id for the next step.
With the SSE client still connected, create a book through the gateway:
curl -X POST http://localhost:8080/packt/inventory/api/books \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"title": "Reactive Spring WebFlux",
"isbn": "978-1617297571",
"authorId": 1,
"price": 49.99,
"genre": "Technology",
"description": "Hands-on reactive Spring guide",
"pageCount": 420
}'Expected results:
- The write request returns
201 Created. - The SSE client receives a
NEW_BOOKevent. - The event payload includes
bookId,bookTitle,isbn, andeventData.authorName.
Patch the same book with a different price:
curl -X PATCH http://localhost:8080/packt/inventory/api/books/<book-id> \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"price": 39.99
}'Expected results:
- The write request returns
200 OK. - The SSE client receives a
PRICE_CHANGEevent. - The payload contains
oldPrice,newPrice,priceChange, andpercentageChange.
The filtered endpoint should only emit the selected event type.
Browser or debug page:
- Choose
NEW_BOOKorPRICE_CHANGEin the stream filter before connecting.
CLI:
./test-sse-curl.sh PRICE_CHANGEThen repeat both write operations and confirm that only matching event frames appear in the filtered subscriber.
Run the status endpoint again while a client is connected:
curl http://localhost:8080/packt/inventory/api/notifications/statusExpected behavior:
activeSubscribersincreases when a stream is open.activeSubscribersdrops after the browser page or terminal subscriber disconnects.
This section provides a deeper, tool-by-tool validation flow so you can prove not only that events are emitted, but that each test client behaves correctly.
Use these shell variables to keep commands repeatable during testing:
export BASE_URL="http://localhost:8080"
export ACCESS_TOKEN="<paste-access-token-here>"Optional helper values for repeated calls:
export AUTHOR_ID="1"
export BOOK_ID="1"Terminal 1 (subscriber):
./test-sse-curl.shTerminal 2 (writes):
- Create or confirm an author.
curl "${BASE_URL}/packt/inventory/api/authors" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"curl -X POST "${BASE_URL}/packt/inventory/api/authors" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"Josh Long","nationality":"American"}'- Create a book and capture
idfrom the response.
curl -X POST "${BASE_URL}/packt/inventory/api/books" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"title": "Reactive Spring WebFlux",
"isbn": "978-1617297571",
"authorId": 1,
"price": 49.99,
"genre": "Technology",
"description": "Hands-on reactive Spring guide",
"pageCount": 420
}'- Patch price for the created book.
curl -X PATCH "${BASE_URL}/packt/inventory/api/books/${BOOK_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"price":39.99}'Validate in Terminal 1:
- you receive a
NEW_BOOKframe after create - you receive a
PRICE_CHANGEframe after patch - each event contains
eventId,bookId, andeventData
- Open
test-sse.html. - Connect to stream using the gateway URL.
- Trigger create and patch API calls.
Validate in dashboard:
Total Eventsincreases by 2New Booksincreases by 1Price Changesincreases by 1Active Subscribersis greater than 0 while connected
Use this scenario to validate user-facing behavior and visual counters.
- Open
test-sse-debug.html. - Connect and click
Check Service Status. - Trigger the same create and patch API calls.
Validate in debug log:
- named events appear as
NEW_BOOKandPRICE_CHANGE - payload structure is complete and parseable JSON
- reconnect attempts are visible if the stream is interrupted
Use this scenario when you need low-level troubleshooting, especially for malformed payloads or missing event names.
Terminal 1:
./test-sse-curl.sh NEW_BOOKTerminal 2:
./test-sse-curl.sh PRICE_CHANGETerminal 3:
- run one create call and one patch call
Expected behavior:
NEW_BOOKterminal receives create events onlyPRICE_CHANGEterminal receives patch events only- no cross-delivery between filtered subscribers
- Query status before subscribing.
- Subscribe from one or more clients.
- Query status again.
- Disconnect clients.
- Query status a final time.
Commands:
curl "${BASE_URL}/packt/inventory/api/notifications/status"Expected behavior:
- subscriber count rises after connect
- subscriber count drops after disconnect
This confirms the SSE lifecycle is managed correctly and helps identify ghost connections.
Mark SSE testing complete when all are true:
- Notification status endpoint is reachable and returns
UP. - At least one subscriber connects from each tool family (CLI and browser).
NEW_BOOKis emitted after successful create.PRICE_CHANGEis emitted after successful patch.- Filtered subscriptions isolate event types correctly.
- Subscriber counts rise and fall as clients connect and disconnect.
The payload examples below are aligned with the current event model in inventory-ms.
{
"eventType": "NEW_BOOK",
"timestamp": "2026-02-15T10:30:00",
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"bookId": 42,
"bookTitle": "Reactive Spring WebFlux",
"isbn": "978-1617297571",
"eventData": {
"authorName": "Josh Long",
"genre": "Technology",
"price": 49.99,
"quantity": 100,
"published": "2025-01-15",
"description": "Hands-on reactive Spring guide",
"pageCount": 420,
"coverImageUrl": null
}
}eventData fields come from NewBookEventData and may be null when optional values are not set.
{
"eventType": "PRICE_CHANGE",
"timestamp": "2026-02-15T10:35:00",
"eventId": "660f9511-f30c-52e5-b827-557766551111",
"bookId": 42,
"bookTitle": "Reactive Spring WebFlux",
"isbn": "978-1617297571",
"eventData": {
"oldPrice": 49.99,
"newPrice": 39.99,
"priceChange": -10.0,
"percentageChange": -20.0
}
}eventData fields come from PriceChangeEventData.
- Confirm the write request succeeded and returned
200or201. - Verify the request carried
Authorization: Bearer ${ACCESS_TOKEN}. - Use
test-sse-debug.htmlto inspect raw frames and confirm events are not filtered out by the UI.
- Use
authorId, notauthorName. - Verify required fields (
title,isbn,authorId,price) are present. - Create or fetch an author first if the id is unknown.
- This is expected if sign-in was skipped or the token expired.
- SSE subscriptions are public, but book and author writes are authenticated.
- Confirm the gateway and inventory service are both running.
- Verify
GET /packt/inventory/api/notifications/statusreturnsUP. - Check gateway timeout and CORS settings if this appears only in browser clients.
- Exit code
22comes from curl when HTTP status is>= 400while--fail-with-bodyis enabled. - Check the gateway URL in
SSE_GATEWAY_URLand verify the notification endpoint path. - Confirm the gateway route for
/packt/inventory/api/notifications/**is active.
The security model is deliberately split:
GET /packt/inventory/api/notifications/**is public.- Write operations under
/packt/inventory/api/**still require authentication.
That separation lets you validate SSE with a simple browser page while still protecting inventory mutations.
Observability remains important for SSE because the most common failures are integration failures:
- Bad routing
- Premature connection timeouts
- Missing event publication after successful writes
- Authentication failures on the write path
Use the notification status endpoint, service logs, and gateway traces together when diagnosing issues.
This chapter ships with the following validation aids:
test-sse-debug.htmltest-sse.htmltest-sse-curl.shSSE-IMPLEMENTATION.md
SSE-IMPLEMENTATION.md remains available as a compact lab companion, while this README now contains the full end-to-end chapter flow.
In this chapter, you added a reactive notification stream to the Bookstore inventory service and validated it end to end through the gateway.
You now have:
- Real-time
NEW_BOOKandPRICE_CHANGEnotifications - A public SSE read path through the gateway
- Authenticated write operations that emit those events
- Browser and CLI tools for stream validation
The next logical step is to consume the stream from a richer frontend or operational dashboard and use the same validation flow to prove those clients stay in sync with the backend.