Build a Reactive Incident Service

Build a reactive Spring WebFlux service with validation, SSE streams, and logs.

Introduction

30 Second Summary

During an incident, responders need each update while it is still useful. A one-time status check can leave the team working from stale information.

In this project, you will build a backend-only Incident Service with Spring Boot and Spring WebFlux. The finished service exposes validated reactive endpoints plus a live stream for new incidents.

What You'll Build

Your final demo shows a live incident stream receiving a newly submitted incident the moment another client creates it.

By the end of this project, you'll have:

  • A filterable JSON snapshot that lets API consumers fetch current incidents through a severity query parameter.
  • A validated submission flow that returns a generated incident ID for valid input. Invalid input produces an HTTP 400 response.
  • A persistent Server-Sent Events stream that delivers new incidents to connected consumers. Structured logs show completed or cancelled requests.
  • Secret Mission: Add server-side severity filtering to replayed and future events in the live stream.

Are there any prerequisites?

This project assumes professional Spring MVC experience plus familiarity with Java, REST, Maven projects, and HTTP.

You also need IntelliJ IDEA on Windows plus a compatible Java Development Kit.

Before We Start

The incident microservice runs on Spring Boot 4.1.1. Its reactive request handling uses Spring WebFlux.

A supported Java runtime lets that stack start cleanly. Windows PowerShell also gives you the native curl client for later HTTP endpoint checks.

In this step, get ready to:
  • Confirm that the installed Java major version is within the supported range.
  • Confirm that curl.exe prints its version information.
Check your Java version

The Java Development Kit supplies the runtime for your service. Spring Boot 4.1.1 supports Java 17 through Java 26.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search field.
  • Press Enter to open Windows PowerShell.
  • Print your installed Java version by running this command:
java -version

What does this command do?

The java -version command prints the Java product version. This identifies the Java installation that PowerShell resolves.

✔️ I see a supported Java version

That clears the first compatibility gate. PowerShell is using Java 17 through Java 26, which Spring Boot 4.1.1 supports.

ⓧ I see an unsupported version

A Java version outside the supported range can prevent the service from starting correctly.

  • Install a Java Development Kit release from 17 through 26 using your organization's approved JDK distribution.
  • Enable the installer option that makes Java available from PowerShell.
  • Close the current PowerShell window.
  • Open a fresh PowerShell window through Windows search.
  • Run the Java version check shown above.
  • Confirm that the reported major version falls from 17 through 26.

ⓧ Command not found

PowerShell cannot reach a Java installation yet.

  • Install a Java Development Kit release from 17 through 26 using your organization's approved JDK distribution.
  • Enable the installer option that makes Java available from PowerShell.
  • Close the current PowerShell window.
  • Open a fresh PowerShell window through Windows search.
  • Run the Java version check shown above.
  • Confirm that PowerShell prints a supported Java version.

Still seeing the wrong Java version?

PowerShell may still be resolving an older Java installation from the existing terminal session.

  • Close every PowerShell window.
  • Open a fresh PowerShell window through Windows search.
  • Repeat the Java version check.

Help me identify why PowerShell is using the wrong Java version.

Confirm the Windows curl client

Windows includes curl.exe. Using the executable name directly avoids the Windows PowerShell curl alias.

Before you run this check, predict whether PowerShell can find the Windows curl client.

  • Print the curl version information by running this command:
curl.exe --version

What does this command do?

The curl.exe --version command prints curl version information. A version summary confirms that the Windows client is available for later endpoint requests.

✔️ I see curl version information

Your second prerequisite is ready. PowerShell can run the Windows curl client that you use to test the incident endpoints.

ⓧ Command not found

Windows includes the curl executable. Missing output means the local Windows installation needs attention before command-line endpoint checks can work.

  • Close the current PowerShell window.
  • Open a fresh PowerShell window through Windows search.
  • Repeat the curl version check shown above.
  • Ask your system administrator to restore the Windows curl executable if the command remains unavailable.
  • Repeat the curl version check after the repair.

Still missing curl output?

A shell alias can change how the word curl behaves. This project always uses curl.exe to call the Windows executable directly.

  • Copy the command exactly as shown above.
  • Check that the command includes the .exe file extension.

Help me diagnose why curl.exe is unavailable in Windows PowerShell.

Your local tools are ready. Next, you'll create the incident-service skeleton without version uncertainty getting in the way.

Set Up the Reactive Microservice

Your Java version and Windows curl client are ready. A clean Spring WebFlux foundation now prevents servlet dependencies or incompatible settings from hiding the reactive execution model.

You will generate an incident-service project with Spring Boot 4.1.1. Its Maven build will use Java 17 with WebFlux and Jakarta Validation.

In this step, get ready to:
  • Generate the incident-service project with reactive web support.
  • Align the build files and create the layered package structure.
  • Run the application locally from IntelliJ IDEA.
Generate the project skeleton

Spring Initializr creates a Maven structure with the wrapper and source directories already in place. Choosing only the reactive web dependency keeps the service on the WebFlux execution path.

  • Open Spring Initializr in your browser.
  • Select Maven for the project type.
  • Select Java for the language.
  • Select 4.1.1 for the Spring Boot version.

The project summary should now show Maven with Java on Spring Boot 4.1.1.

  • Enter com.nextwork in the Group field.
  • Enter incident-service in the Artifact field.
  • Enter incident-service in the Name field.
  • Enter Reactive incident management microservice in the Description field.
  • Enter com.nextwork.incidents in the Package name field.
  • Select 17 for the Java version.

The metadata now identifies one focused incident microservice. The package name also gives Spring Boot a base package for component scanning.

  • Click Add Dependencies.
  • Search for Spring Reactive Web.
  • Select Spring Reactive Web.
  • Click Generate to download the project archive.

Your browser downloads the generated incident-service archive. It contains the Maven wrapper and the standard source tree.

  • Open your Downloads folder in File Explorer.
  • Right-click the downloaded incident-service archive.
  • Select Extract All.
  • Choose your Desktop as the extraction destination.
  • Click Extract.

You should see an incident-service folder on your Desktop. Inside it, you should find pom.xml and the src directory.

  • Press the Windows key to open Windows search.
  • Type IntelliJ IDEA and press Enter.
  • Click Open on the IntelliJ IDEA welcome screen.
  • Open the incident-service folder on your Desktop.
  • Select pom.xml to open it as a Maven project.

The first Maven import can take a moment while IntelliJ IDEA resolves the project model. You should then see pom.xml and src in the Project tool window.

Project Not Importing as Maven?

Make sure you selected the pom.xml inside the extracted incident-service folder. Selecting the downloaded archive leaves IntelliJ IDEA without an imported Maven project.

If dependency resolution stalls, confirm that IntelliJ IDEA can access the internet. Help me diagnose an IntelliJ Maven import problem.

Align the Maven build

The build file defines the service boundary before you add endpoints. Its dependency list includes WebFlux without the servlet web starter, while Validation prepares the project for controlled request input.

  • Open pom.xml from the Project tool window.
  • Select the current contents of pom.xml.
  • Replace the selected contents with this Maven configuration:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.1</version>
    </parent>
    <groupId>com.nextwork</groupId>
    <artifactId>incident-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>incident-service</name>
    <description>Reactive incident management microservice</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>
</project>

What Does This Configuration Do?

  • The parent coordinates pin the project to Spring Boot 4.1.1.
  • The java.version property sets Java 17 as the build target.
  • The WebFlux starter supplies the reactive web stack.
  • The Validation starter supplies Jakarta request validation support.
  • Save pom.xml.
  • Wait for IntelliJ IDEA to reload the Maven model.
  • Confirm that the two dependency artifact names have no red unresolved markers.

Dependencies Still Unresolved?

Check that the parent version is exactly 4.1.1. A missing closing XML tag can also prevent the entire Maven model from loading.

Use IntelliJ IDEA's Maven reload action after saving the file. Help me find the problem in my Maven configuration.

The Spring Boot Maven plugin connects the project model to Spring Boot's packaging and build support. It belongs inside pom.xml before the final project closing tag.

  • Find the final </project> line in pom.xml.
  • Paste this block immediately above that closing tag:
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Why Add the Build Plugin?

The Spring Boot Maven plugin gives Maven the Spring Boot build lifecycle. The parent manages its version alongside the rest of the project.

  • Save pom.xml.
  • Confirm that the Maven reload finishes without a build-model error.

Seeing an XML Build Error?

Make sure the build block sits after dependencies and before </project>. Check that only one final project closing tag remains.

Help me place the Spring Boot Maven plugin correctly.

The application class is the entry point that asks Spring Boot to scan com.nextwork.incidents and start the service. Keeping it above the feature packages lets future controllers and services participate in component scanning.

  • Open src/main/java/com/nextwork/incidents/IncidentApiApplication.java from the Project tool window.
  • Select the generated class contents.
  • Replace the selected contents with this application entry point:
package com.nextwork.incidents;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class IncidentApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(IncidentApiApplication.class, args);
    }
}

What Does This Class Do?

  • The package declaration places the entry point at the top of the service's package tree.
  • The SpringBootApplication annotation enables configuration and component scanning.
  • The main() method hands control to Spring Boot through SpringApplication.run().
  • Save src/main/java/com/nextwork/incidents/IncidentApiApplication.java.
  • Confirm that IntelliJ IDEA recognizes both Spring imports without red unresolved markers.

Spring Imports Showing Red?

Return to pom.xml and confirm the WebFlux dependency is present. Reload the Maven model after every build-file correction.

Help me resolve the Spring imports in IncidentApiApplication.

Create the packages and run the service

A layered package structure gives each responsibility a clear home before features arrive. The configuration file also names the process and switches its console output to structured JSON logs.

  • Open the Project tool window with Alt+1.
  • Right-click the com.nextwork.incidents package.
  • Select New.
  • Select Package.
  • Enter controller and press Enter.
  • Repeat the package action for domain.

The Project tool window should now show controller and domain beneath com.nextwork.incidents.

  • Create the dto package beneath com.nextwork.incidents.
  • Create the logging package beneath com.nextwork.incidents.

The package tree should now include controller, domain, dto, and logging.

  • Create the service package beneath com.nextwork.incidents.
  • Open src/main/resources/application.properties from the Project tool window.
  • Replace its contents with these application settings:
spring.application.name=incident-service
logging.level.com.nextwork.incidents=INFO
logging.structured.format.console=logstash

What Do These Settings Control?

  • The application name identifies this process as incident-service in Spring Boot output.
  • The package logging level keeps the service's future business and request logs visible at INFO.
  • The structured console setting formats Logback output as Logstash-style JSON.
  • Save src/main/resources/application.properties.
  • Confirm that the Project tool window lists the five new packages beneath com.nextwork.incidents.

Package Missing From the Project Tree?

Create each package beneath the Java source package at com.nextwork.incidents. A package created under resources will appear in the wrong part of the project.

Help me correct my IntelliJ package structure.

Use this reference to compare every configured file before the first launch.

✔️ Awesome, I've got everything!

Great. Save all three files before you start the application.

ⓧ I'd like to double check the full code

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.1</version>
    </parent>
    <groupId>com.nextwork</groupId>
    <artifactId>incident-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>incident-service</name>
    <description>Reactive incident management microservice</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
package com.nextwork.incidents;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class IncidentApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(IncidentApiApplication.class, args);
    }
}
spring.application.name=incident-service
logging.level.com.nextwork.incidents=INFO
logging.structured.format.console=logstash

Before you launch the service, what application name and startup message do you expect the console to report?

  • Return to src/main/java/com/nextwork/incidents/IncidentApiApplication.java.
  • Click the run icon in the editor gutter beside main().
  • Select Run 'IncidentApiApplication.main()'.

The first launch can take a moment while Maven finishes resolving the application runtime. Keep the process running when the Run tool window settles.

You should see Started IncidentApiApplication in the console. The structured output should identify the application as incident-service, while the service listens locally on port 8080.

That is the foundation working. Your reactive microservice now starts with the intended build, package boundary, and logging configuration.

Application Not Starting?

If IntelliJ IDEA reports a busy port, stop the other process using port 8080 before running this service again. If compilation fails, compare pom.xml and IncidentApiApplication.java with the full-code tab.

Confirm that the project SDK uses the compatible JDK you verified earlier. Help me diagnose why IncidentApiApplication will not start.

Your reactive service skeleton is live. Next, you will connect a controller to a constructor-injected service and return the first incident snapshot.

Build the Incident Snapshot Endpoint

Your Spring Boot service already starts on port 8080. It still cannot represent an incident or answer an API request.

A visible JSON response proves that component scanning finds your controller. It also proves that constructor injection connects the controller to a Spring WebFlux service.

In this step, get ready to:
  • Model each incident as an immutable Java record.
  • Expose three seeded incidents through a reactive service.
  • Return the incident snapshot from a constructor-injected controller.
Create the incident record

An Incident needs one stable shape throughout the service. A Java record provides immutable fields for its identity, title, severity, and creation time.

  • In the Project tool window in IntelliJ IDEA, switch back to src/main/java/com/nextwork/incidents/domain.
  • Right-click the domain package.
  • Select New.
  • Select Java Class.
  • Name the file Incident.
  • Replace the generated contents of Incident.java by pasting this code:
package com.nextwork.incidents.domain;

import java.time.Instant;
import java.util.UUID;

public record Incident(UUID id, String title, String severity, Instant createdAt) {
}

What does this record define?

  • The UUID field gives each incident a unique identifier.
  • The title field describes the operational problem.
  • The severity field stores the incident priority.
  • The Instant field records when the incident was created.
  • The record declaration keeps these values immutable after construction.
  • Save Incident.java.
  • Restart IncidentApiApplication using the IntelliJ run controls.

You'll see the application return to its running state without a compilation failure.

Incident record not compiling?

  • Confirm that the package declaration matches the domain folder.
  • Compare the imported Java types with the reference code.

Still stuck? Help me diagnose why Incident.java does not compile.

✔️ Awesome, I've got everything!

Good progress. Your immutable incident shape now compiles.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.domain;

import java.time.Instant;
import java.util.UUID;

public record Incident(UUID id, String title, String severity, Instant createdAt) {
}
Build the reactive incident service

The service owns the incident collection while Project Reactor controls how callers receive it. Each subscription gets a copied snapshot through a Flux<Incident>.

  • In the Project tool window, switch back to src/main/java/com/nextwork/incidents/service.
  • Right-click the service package.
  • Select New.
  • Select Java Class.
  • Name the file IncidentService.
  • Replace the generated contents of IncidentService.java by pasting this code:
package com.nextwork.incidents.service;

import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import com.nextwork.incidents.domain.Incident;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
public class IncidentService {
    private final List<Incident> incidents = new CopyOnWriteArrayList<>();

    public IncidentService() {
        seed("Checkout API latency", "HIGH");
        seed("Email delivery delay", "MEDIUM");
        seed("Metrics exporter offline", "CRITICAL");
    }

    public Flux<Incident> findAll() {
        return Flux.defer(() -> Flux.fromIterable(List.copyOf(this.incidents)));
    }

    private void seed(String title, String severity) {
        this.incidents.add(new Incident(UUID.randomUUID(), title, severity, Instant.now()));
    }
}

What does this service do?

  • The @Service annotation makes IncidentService available for component scanning.
  • The CopyOnWriteArrayList stores the current incidents in memory.
  • The constructor seeds three incidents when Spring creates the service.
  • The Flux.defer() call waits for a subscription before reading the collection.
  • The List.copyOf() call gives each subscriber a stable snapshot.
  • The method returns the Flux directly. It never calls block() or subscribe().
  • Save IncidentService.java.
  • Restart IncidentApiApplication using the IntelliJ run controls.

You'll see the service start successfully with all three seed records created in memory.

Service not starting?

  • Confirm that IncidentService.java sits inside the service package.
  • Check that the Incident import uses the com.nextwork.incidents.domain package.
  • Compare all three seed() calls with the reference code.

Need another pair of eyes? Help me find the startup problem in IncidentService.java.

✔️ Awesome, I've got everything!

Your service now owns three incidents and exposes them as a reactive snapshot.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.service;

import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import com.nextwork.incidents.domain.Incident;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
public class IncidentService {
    private final List<Incident> incidents = new CopyOnWriteArrayList<>();

    public IncidentService() {
        seed("Checkout API latency", "HIGH");
        seed("Email delivery delay", "MEDIUM");
        seed("Metrics exporter offline", "CRITICAL");
    }

    public Flux<Incident> findAll() {
        return Flux.defer(() -> Flux.fromIterable(List.copyOf(this.incidents)));
    }

    private void seed(String title, String severity) {
        this.incidents.add(new Incident(UUID.randomUUID(), title, severity, Instant.now()));
    }
}
Connect the snapshot endpoint

An annotated controller connects an HTTP route to the service. The controller collects the source Flux<Incident> into one Mono<List<Incident>> response.

The constructor receives IncidentService as a mandatory dependency. Spring supplies the service when it creates the controller.

  • In the Project tool window, switch back to src/main/java/com/nextwork/incidents/controller.
  • Right-click the controller package.
  • Select New.
  • Select Java Class.
  • Name the file IncidentController.
  • Replace the generated contents of IncidentController.java by pasting this code:
package com.nextwork.incidents.controller;

import java.util.List;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.service.IncidentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/incidents")
public class IncidentController {
    private final IncidentService incidentService;

    public IncidentController(IncidentService incidentService) {
        this.incidentService = incidentService;
    }

    @GetMapping
    public Mono<List<Incident>> list() {
        return this.incidentService.findAll().collectList();
    }
}

How does the endpoint work?

  • The @RestController annotation registers the class for HTTP request handling.
  • The @RequestMapping("/api/incidents") annotation sets the controller's route.
  • The constructor gives the controller its IncidentService dependency.
  • The @GetMapping annotation maps GET requests to list().
  • The collectList() operator collects the emitted incidents into one list.
  • The returned Mono lets WebFlux write that list as a JSON response.
  • Save IncidentController.java.
  • Restart IncidentApiApplication using the IntelliJ run controls.

You'll see the application return to its running state on port 8080.

Controller not loading?

  • Confirm that IncidentController.java sits inside the controller package.
  • Check that the constructor parameter is named incidentService.
  • Compare the route annotations with the reference code.

If the application still stops during startup, help me debug the IncidentController wiring.

✔️ Awesome, I've got everything!

The controller now connects the HTTP route to your incident service.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.controller;

import java.util.List;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.service.IncidentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/incidents")
public class IncidentController {
    private final IncidentService incidentService;

    public IncidentController(IncidentService incidentService) {
        this.incidentService = incidentService;
    }

    @GetMapping
    public Mono<List<Incident>> list() {
        return this.incidentService.findAll().collectList();
    }
}
  • Press the Windows key to open the search bar.
  • Type PowerShell and press Enter.

Before you send the request, how many seeded incidents do you expect the endpoint to return?

  • Request the incident snapshot by running this command:
curl.exe http://localhost:8080/api/incidents

What should I see?

You'll see a JSON array containing three incident objects.

  • One object has the title Checkout API latency.
  • One object has the title Email delivery delay.
  • One object has the title Metrics exporter offline.
  • Every object includes an id value.
  • Every object includes a createdAt value.

Snapshot request not working?

  • Confirm that IncidentApiApplication is still running on port 8080.
  • Compare @RequestMapping("/api/incidents") with the controller reference code.
  • Use curl.exe from Windows PowerShell.

Still missing the JSON array? Help me debug the GET incident snapshot request.

That's your first reactive API response working. The request now travels through the controller into the service before returning three incidents as JSON.

Your incident snapshot endpoint works. Next, you'll give the API controlled ways to filter and accept incident data.

Add Filtering and Request Validation

Your first Spring WebFlux endpoint already returns a reactive snapshot of the seeded incidents. That response proves the controller can reach the service through constructor injection.

Incoming values are still unrestricted. This step adds controlled request input so clients can filter incidents safely and submit validated incident data.

In this step, get ready to:
  • Filter incident snapshots through an optional severity query parameter.
  • Define the validation rules for incoming incident data.
  • Create incidents through a POST endpoint with business-event logging.
Filter incident snapshots by severity

A query parameter lets an API consumer narrow a response without creating a separate endpoint. An empty parameter still returns the full snapshot.

  • Switch back to IntelliJ IDEA.
  • Open src/main/java/com/nextwork/incidents/service/IncidentService.java from the project panel.
  • Replace the Java utility import section with this block:
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

Why add Optional?

The Optional<String> value represents a severity filter that may be absent. The service can use one method for filtered requests and unfiltered requests.

The Locale import supports the uppercase conversion used when the service creates incidents later in this step.

  • Find the method that starts with public Flux<Incident> findAll().
  • Replace that method with this filtered version:
    public Flux<Incident> findAll(Optional<String> severity) {
        Flux<Incident> source = Flux.defer(() -> Flux.fromIterable(List.copyOf(this.incidents)));
        return severity
                .filter(value -> !value.isBlank())
                .map(value -> source.filter(incident -> incident.severity().equalsIgnoreCase(value)))
                .orElse(source);
    }

What does this filter do?

  • The source variable keeps the deferred copy of the current incident list.
  • The first filter ignores a blank severity value.
  • The second filter keeps incidents whose severity matches without case sensitivity.
  • The orElse(source) branch returns every incident when the query parameter is absent.
  • Open src/main/java/com/nextwork/incidents/controller/IncidentController.java from the project panel.
  • Add java.util.Optional below the existing java.util.List import using this block:
import java.util.List;
import java.util.Optional;

What does this import support?

The controller converts the nullable query parameter into an Optional before passing it to the service. This keeps the filtering decision inside IncidentService.

  • Find the RequestMapping import in the controller.
  • Replace the surrounding request-mapping imports with this block:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

What does RequestParam do?

The RequestParam annotation binds the URL query parameter to a controller method argument. Setting required = false allows requests that omit severity.

  • Find the controller method that starts with public Mono<List<Incident>> list().
  • Replace that method with this parameter-aware version:
    @GetMapping
    public Mono<List<Incident>> list(@RequestParam(required = false) String severity) {
        return this.incidentService.findAll(Optional.ofNullable(severity)).collectList();
    }

How does the request reach the filter?

  • The controller receives the optional severity value from the URL.
  • The Optional.ofNullable(severity) call represents a supplied value or an absent value safely.
  • The service filters its Flux before collectList() produces the snapshot Mono.
  • Save your changes in IntelliJ IDEA.
  • Restart IncidentApiApplication from the existing run configuration.

You should see the application start successfully on port 8080.

  • Press the Windows key to open system search.
  • Type PowerShell into the search field.
  • Press Enter to open Windows PowerShell.

Before you send the request, predict how many seeded incidents have a severity of HIGH.

  • Check the severity filter by running this command:
curl.exe "http://localhost:8080/api/incidents?severity=HIGH"

What should I see?

You should see a JSON array containing only the incident titled Checkout API latency. The other seeded incidents have different severity values.

Good work. The snapshot endpoint now gives API consumers a focused incident view without changing the endpoint path.

Seeing every incident?

Confirm that IncidentController.list() passes Optional.ofNullable(severity) to the service.

Check that the service applies source.filter inside the present severity branch.

Ask for help with the filter if your response still contains every incident. Help me debug the severity query parameter in my Spring WebFlux incident endpoint.

Validate incoming incident requests

A request DTO defines the exact data a client may submit. Jakarta Validation then rejects missing titles, oversized titles, and unsupported severity values before the controller calls the service.

  • Select the existing src/main/java/com/nextwork/incidents/dto package in the project panel.
  • Create CreateIncidentRequest.java inside that package.
  • Paste this validated request record into the new file:
package com.nextwork.incidents.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record CreateIncidentRequest(
        @NotBlank(message = "title is required")
        @Size(max = 120, message = "title must contain at most 120 characters")
        String title,
        @NotBlank(message = "severity is required")
        @Pattern(regexp = "LOW|MEDIUM|HIGH|CRITICAL", message = "severity must be LOW, MEDIUM, HIGH, or CRITICAL")
        String severity
) {
}

What do these rules protect?

  • The NotBlank annotations require visible content for both fields.
  • The Size annotation limits titles to 120 characters.
  • The Pattern annotation accepts the four severity values used by the incident domain.
  • Save CreateIncidentRequest.java.
  • Restart IncidentApiApplication from the existing run configuration.

You should see Started IncidentApiApplication again. This confirms the new validation record compiles inside the application.

  • Return to src/main/java/com/nextwork/incidents/service/IncidentService.java.
  • Replace the complete import section with this block:
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.dto.CreateIncidentRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

What do the new imports enable?

The service now accepts CreateIncidentRequest values and returns created incidents through a Mono.

The logger imports provide parameterized business-event logging through SLF4J.

  • Find the opening of the IncidentService class.
  • Replace the class opening with this logger-enabled version:
public class IncidentService {
    private static final Logger log = LoggerFactory.getLogger(IncidentService.class);
    private final List<Incident> incidents = new CopyOnWriteArrayList<>();

Why keep one class logger?

The static logger records events owned by IncidentService. Each creation log can identify the new incident without printing the entire request body.

  • Save IncidentService.java.
  • Restart IncidentApiApplication from the existing run configuration.

You should see the application start successfully with the logger field in place.

  • Find the closing brace of findAll() in IncidentService.java.
  • Add this creation method immediately below findAll():
    public Mono<Incident> create(CreateIncidentRequest request) {
        return Mono.fromSupplier(() -> {
            Incident incident = new Incident(
                    UUID.randomUUID(),
                    request.title(),
                    request.severity().toUpperCase(Locale.ROOT),
                    Instant.now()
            );
            this.incidents.add(incident);
            log.info("incidentCreated id={} severity={}", incident.id(), incident.severity());
            return incident;
        });
    }

What does the creation pipeline do?

  • The Mono.fromSupplier callback creates the incident when the reactive pipeline is subscribed to.
  • The new record receives a generated identifier and the current timestamp.
  • The severity is normalized to uppercase before storage.
  • The structured business log records the generated ID and normalized severity.
  • Save IncidentService.java.
  • Restart IncidentApiApplication from the existing run configuration.

You should see Started IncidentApiApplication in the console. The service is ready for a controller to call create().

Application no longer starts?

Confirm that CreateIncidentRequest.java uses the package com.nextwork.incidents.dto.

Check that the service imports both CreateIncidentRequest and Reactor's Mono.

Use this prompt if the compiler points to the new request record or creation method. Help me debug the compilation error in CreateIncidentRequest or IncidentService.create.

Create incidents and inspect the response

The controller is the validation boundary for incoming JSON. Applying @Valid to the request body produces an HTTP 400 response when the DTO rules fail.

  • Return to src/main/java/com/nextwork/incidents/controller/IncidentController.java.
  • Replace the complete import section with this block:
import java.util.List;
import java.util.Optional;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.dto.CreateIncidentRequest;
import com.nextwork.incidents.service.IncidentService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

What does the controller need?

The controller imports the validated request record and the annotations for POST request handling. The existing constructor-injected service remains the only dependency.

  • Find the closing brace of the list() method.
  • Add this POST endpoint immediately below list():
    @PostMapping
    public Mono<Incident> create(@Valid @RequestBody CreateIncidentRequest request) {
        return this.incidentService.create(request);
    }

How does validation enter the pipeline?

  • The PostMapping annotation maps POST requests to the collection endpoint.
  • The RequestBody annotation converts the submitted JSON into CreateIncidentRequest.
  • The Valid annotation checks the DTO constraints before IncidentService.create() runs.
  • Save IncidentController.java.
  • Restart IncidentApiApplication from the existing run configuration.

You should see Started IncidentApiApplication in the IntelliJ console.

Before you submit invalid JSON, predict whether the controller or the service rejects the blank title first.

  • Return to the PowerShell window from earlier.
  • Submit an empty title by running this command:
curl.exe -i -X POST http://localhost:8080/api/incidents -H "Content-Type: application/json" -d '{"title":"","severity":"HIGH"}'

What should I see?

You should see an HTTP 400 response. Validation rejects the request before the service creates or stores an incident.

That rejection proves the request boundary is protecting the incident list from invalid data.

Before you submit valid JSON, predict which fields the service generates for the new incident.

  • Create a valid incident by running this command:
curl.exe -X POST http://localhost:8080/api/incidents -H "Content-Type: application/json" -d '{"title":"Database latency","severity":"HIGH"}'

What should I see?

You should see a JSON incident containing a generated id and the title Database latency. The response also contains the uppercase severity and a creation timestamp.

In the IntelliJ console, you should see an incidentCreated business log containing the generated ID and severity.

You now have both sides of controlled API input. Invalid requests stop at validation while valid requests create stored incidents and produce an operational log.

POST request not behaving as expected?

If invalid JSON reaches the service, confirm that the controller parameter contains both @Valid and @RequestBody.

If a valid request returns an error, check that the JSON property names match title and severity.

Ask for help if the response or log still differs. Help me debug my validated incident POST endpoint and its business log.

Compare your three modified files with the completed versions below.

✔️ Awesome, I've got everything!

Your request DTO, service, and controller now match the completed filtering and validation implementation. Save every modified file before continuing.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record CreateIncidentRequest(
        @NotBlank(message = "title is required")
        @Size(max = 120, message = "title must contain at most 120 characters")
        String title,
        @NotBlank(message = "severity is required")
        @Pattern(regexp = "LOW|MEDIUM|HIGH|CRITICAL", message = "severity must be LOW, MEDIUM, HIGH, or CRITICAL")
        String severity
) {
}

Check the request record

Compare the package name, imports, validation messages, and record fields exactly. These constraints define the controller's accepted request shape.

package com.nextwork.incidents.service;

import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.dto.CreateIncidentRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class IncidentService {
    private static final Logger log = LoggerFactory.getLogger(IncidentService.class);
    private final List<Incident> incidents = new CopyOnWriteArrayList<>();

    public IncidentService() {
        seed("Checkout API latency", "HIGH");
        seed("Email delivery delay", "MEDIUM");
        seed("Metrics exporter offline", "CRITICAL");
    }

    public Flux<Incident> findAll(Optional<String> severity) {
        Flux<Incident> source = Flux.defer(() -> Flux.fromIterable(List.copyOf(this.incidents)));
        return severity
                .filter(value -> !value.isBlank())
                .map(value -> source.filter(incident -> incident.severity().equalsIgnoreCase(value)))
                .orElse(source);
    }

    public Mono<Incident> create(CreateIncidentRequest request) {
        return Mono.fromSupplier(() -> {
            Incident incident = new Incident(
                    UUID.randomUUID(),
                    request.title(),
                    request.severity().toUpperCase(Locale.ROOT),
                    Instant.now()
            );
            this.incidents.add(incident);
            log.info("incidentCreated id={} severity={}", incident.id(), incident.severity());
            return incident;
        });
    }

    private void seed(String title, String severity) {
        this.incidents.add(new Incident(UUID.randomUUID(), title, severity, Instant.now()));
    }
}

Check the service

Confirm that findAll() accepts the optional filter. Confirm that create() stores the generated incident and writes the business log.

package com.nextwork.incidents.controller;

import java.util.List;
import java.util.Optional;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.dto.CreateIncidentRequest;
import com.nextwork.incidents.service.IncidentService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/incidents")
public class IncidentController {
    private final IncidentService incidentService;

    public IncidentController(IncidentService incidentService) {
        this.incidentService = incidentService;
    }

    @GetMapping
    public Mono<List<Incident>> list(@RequestParam(required = false) String severity) {
        return this.incidentService.findAll(Optional.ofNullable(severity)).collectList();
    }

    @PostMapping
    public Mono<Incident> create(@Valid @RequestBody CreateIncidentRequest request) {
        return this.incidentService.create(request);
    }
}

Check the controller

Confirm that the GET method passes an optional severity to the service. Confirm that the POST method validates the request body before delegating creation.

Your service now filters snapshots and protects incident creation with validation. Next, you will expose a finite reactive feed and watch where snapshot-style publishing falls short.

Expose the Finite Feed

Your service now filters incident snapshots. It also rejects invalid requests before they reach the service layer.

Operators need incident updates over time. This step tests whether a delayed reactive feed can deliver incidents created after its initial data.

In this step, get ready to:
  • Add a delayed finite feed to IncidentService.
  • Map GET /api/incidents/feed to the finite publisher.
  • Test whether the feed receives incidents created after its initial emissions.
Build the delayed finite feed

A finite publisher represents a bounded set of values. Project Reactor's Flux.delayElements(Duration) spaces each current item over time so you can watch the sequence behave.

  • Switch back to IncidentService.java in IntelliJ IDEA.
  • Add the duration import above the existing Instant import:
import java.time.Duration;

What Does This Import Enable?

This import provides the time value used for the delay between incidents. The feed can now express its one-second interval directly in Java.

  • Find the create(CreateIncidentRequest request) method in IncidentService.java.
  • Add the finite feed method immediately below it:
    public Flux<Incident> finiteFeed() {
        return findAll(Optional.empty()).delayElements(Duration.ofSeconds(1));
    }

What Does This Code Do?

  • The findAll(Optional.empty()) call captures the current unfiltered incident sequence.
  • The delayElements operator spaces emissions one second apart.
  • The returned Flux<Incident> remains non-blocking while Reactor manages the timing.
  • Save IncidentService.java.
  • Stop the current incident-service process using the same IntelliJ IDEA control from earlier.
  • Start IncidentApiApplication again using the same run control.

Good work. The service starts successfully with the delayed publisher ready for a controller route.

Service Failing to Restart?

Check that the duration import sits with the other Java imports. Confirm that finiteFeed() is inside the IncidentService class.

Compare the method braces with the reference below. A misplaced closing brace can leave the method outside the class.

Help me diagnose the finite feed compilation problem.

✔️ Awesome, I've got everything!

  • Confirm that IncidentService.java is saved before continuing.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.service;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.dto.CreateIncidentRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class IncidentService {
    private static final Logger log = LoggerFactory.getLogger(IncidentService.class);
    private final List<Incident> incidents = new CopyOnWriteArrayList<>();

    public IncidentService() {
        seed("Checkout API latency", "HIGH");
        seed("Email delivery delay", "MEDIUM");
        seed("Metrics exporter offline", "CRITICAL");
    }

    public Flux<Incident> findAll(Optional<String> severity) {
        Flux<Incident> source = Flux.defer(() -> Flux.fromIterable(List.copyOf(this.incidents)));
        return severity
                .filter(value -> !value.isBlank())
                .map(value -> source.filter(incident -> incident.severity().equalsIgnoreCase(value)))
                .orElse(source);
    }

    public Mono<Incident> create(CreateIncidentRequest request) {
        return Mono.fromSupplier(() -> {
            Incident incident = new Incident(
                    UUID.randomUUID(),
                    request.title(),
                    request.severity().toUpperCase(Locale.ROOT),
                    Instant.now()
            );
            this.incidents.add(incident);
            log.info("incidentCreated id={} severity={}", incident.id(), incident.severity());
            return incident;
        });
    }

    public Flux<Incident> finiteFeed() {
        return findAll(Optional.empty()).delayElements(Duration.ofSeconds(1));
    }

    private void seed(String title, String severity) {
        this.incidents.add(new Incident(UUID.randomUUID(), title, severity, Instant.now()));
    }
}

What Should Match?

This reference shows the new duration import and the complete finiteFeed() method in their final positions.

Expose the feed endpoint

The service method needs an HTTP route before a client can subscribe. The controller returns the Flux<Incident> directly so WebFlux controls subscription and response completion.

  • Switch to IncidentController.java in IntelliJ IDEA.
  • Add the Flux import immediately above the existing Mono import:
import reactor.core.publisher.Flux;

Why Add This Import?

The controller now needs the multi-value reactive return type used by the feed method. The existing snapshot and create endpoints continue using Mono.

  • Save IncidentController.java.
  • Stop the current application process.
  • Start IncidentApiApplication again.

The successful startup confirms that the controller recognizes both reactive return types.

  • Find the create(@Valid @RequestBody CreateIncidentRequest request) method in IncidentController.java.
  • Add the feed route immediately below it:
    @GetMapping("/feed")
    public Flux<Incident> feed() {
        return this.incidentService.finiteFeed();
    }

What Does This Route Do?

  • The @GetMapping("/feed") annotation maps requests beneath the controller's existing base path.
  • The controller delegates the sequence to finiteFeed() through constructor-injected IncidentService.
  • The route declares no infinite-stream media type. Its response follows the lifecycle of the finite publisher.
  • Save IncidentController.java.
  • Stop the running application process.
  • Start IncidentApiApplication again.

The application starts successfully with the feed route registered.

Controller Not Compiling?

Confirm that the new import uses reactor.core.publisher.Flux. Check that the feed() method sits inside IncidentController.

Help me fix the finite feed controller route.

✔️ Awesome, I've got everything!

  • Confirm that IncidentController.java is saved before testing the route.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.controller;

import java.util.List;
import java.util.Optional;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.dto.CreateIncidentRequest;
import com.nextwork.incidents.service.IncidentService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/incidents")
public class IncidentController {
    private final IncidentService incidentService;

    public IncidentController(IncidentService incidentService) {
        this.incidentService = incidentService;
    }

    @GetMapping
    public Mono<List<Incident>> list(@RequestParam(required = false) String severity) {
        return this.incidentService.findAll(Optional.ofNullable(severity)).collectList();
    }

    @PostMapping
    public Mono<Incident> create(@Valid @RequestBody CreateIncidentRequest request) {
        return this.incidentService.create(request);
    }

    @GetMapping("/feed")
    public Flux<Incident> feed() {
        return this.incidentService.finiteFeed();
    }
}

What Should Match?

This reference shows the new Flux import and the complete feed() controller method in their final positions.

Watch the finite feed close

The route is ready for its operational test. You will observe the current incidents first before checking what happens to an incident created afterward.

Before you run this, do you expect the feed to keep waiting after it emits the current incidents?

  • Request the feed from Windows PowerShell by running this command:
curl.exe --no-buffer http://localhost:8080/api/incidents/feed

What Should You See?

The --no-buffer flag displays the response as curl receives it. You will see the current incidents arrive as JSON roughly one second apart.

After the current incidents arrive, the command returns to the PowerShell prompt. The HTTP response has completed.

That return to the prompt is the intended limitation. The client no longer has an open subscription for later incidents.

  • Create another incident from the returned PowerShell prompt by running this command:
curl.exe -X POST http://localhost:8080/api/incidents -H "Content-Type: application/json" -d '{"title":"Database latency","severity":"HIGH"}'

What Does This Prove?

The returned incident response confirms that the POST request succeeded. Its generated ID shows that the service stored a new incident.

The earlier feed remains closed. It cannot emit this later incident because its publisher has already completed.

You have now caught the gap in action. The feed can pace a snapshot over time, but its closed connection cannot notify the client about future incidents.

Feed Not Behaving as Expected?

Confirm that incident-service is still running on port 8080. Restart the application if IntelliJ IDEA is still running an older build.

Check that feed() calls finiteFeed(). Confirm that the service method uses the unfiltered snapshot before applying its delay.

Help me troubleshoot the finite feed test.

You proved that delayed emissions alone cannot create a persistent update channel. Next, you will replace this limitation with a live Server-Sent Events stream and add request-completion logging.

Add a Live Stream and Request Logging

Your finite feed proved that delayed events can still belong to a publisher that completes. Once that client disconnects, incidents created later have nowhere to go.

The fix uses a bounded replaying hot publisher from Project Reactor. You will expose that publisher through Server-Sent Events.

A persistent connection also needs lifecycle visibility. A Spring WebFlux filter will record completed requests plus cancelled streams without blocking the reactive chain.

In this step, get ready to:
  • Publish seeded incidents plus newly created incidents through a bounded replaying sink.
  • Expose a persistent incident stream using an SSE response.
  • Record structured request logs when reactive exchanges complete or get cancelled.
Back the stream with a replaying sink

A Sinks.Many<Incident> accepts incidents from the service and exposes them as a Flux. Its replay limit keeps the latest 20 incidents available for new subscribers.

  • In IncidentService.java, replace the Reactor imports through the service field declarations with this code:
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;

@Service
public class IncidentService {
    private static final Logger log = LoggerFactory.getLogger(IncidentService.class);
    private final List<Incident> incidents = new CopyOnWriteArrayList<>();
    private final Sinks.Many<Incident> incidentStream = Sinks.many().replay().limit(20);

What does this code do?

  • The Sinks import makes Reactor's programmatic publisher API available.
  • The incidentStream field stores a bounded replay history for subscribers.
  • The limit of 20 prevents the replay history from growing without a bound.
  • Save IncidentService.java.
  • Restart IncidentApiApplication.

You should see Started IncidentApiApplication in the IntelliJ IDEA console. This confirms the sink declaration compiles inside the service.

Application not starting?

Check that the Sinks import sits with the other Reactor imports. Confirm that incidentStream is declared inside IncidentService.

If the compiler still highlights this section, help me check the replaying sink declaration.

The seed method now needs to publish each startup incident into the same replay history. Emission failures should stop startup because an incomplete seed history would make new subscribers see inconsistent data.

  • Replace the existing seed() method in IncidentService.java with this version:
    private void seed(String title, String severity) {
        Incident incident = new Incident(UUID.randomUUID(), title, severity, Instant.now());
        this.incidents.add(incident);
        this.incidentStream.tryEmitNext(incident).orThrow();
    }

How does seeding reach the stream?

  • Each seeded Incident is stored in the snapshot list.
  • The same incident is emitted into incidentStream for replay to future subscribers.
  • The orThrow() call prevents the application from silently starting with a failed seed emission.
  • Save IncidentService.java.
  • Restart IncidentApiApplication.

You should see Started IncidentApiApplication again. A successful startup proves all three seed emissions entered the replaying sink without failing.

Seed emission stopping startup?

Check that incidentStream is initialized before the constructor calls seed(). Confirm that each incident is emitted only after it is created.

For help tracing the startup failure, help me debug the seed emission.

New incidents need one consistent outcome across both destinations. The service removes an incident from the in-memory list if publishing fails, so the snapshot and live stream do not disagree.

  • Replace the existing create() method in IncidentService.java with this version:
    public Mono<Incident> create(CreateIncidentRequest request) {
        return Mono.fromSupplier(() -> {
            Incident incident = new Incident(
                    UUID.randomUUID(),
                    request.title(),
                    request.severity().toUpperCase(Locale.ROOT),
                    Instant.now()
            );
            this.incidents.add(incident);
            Sinks.EmitResult emitResult = this.incidentStream.tryEmitNext(incident);
            if (emitResult.isFailure()) {
                this.incidents.remove(incident);
                throw new IllegalStateException("Unable to publish incident: " + emitResult);
            }
            log.info("incidentCreated id={} severity={}", incident.id(), incident.severity());
            return incident;
        });
    }

How does creation stay consistent?

  • The supplier creates the incident only when the returned Mono is subscribed to.
  • The service adds the incident to the in-memory snapshot before attempting to publish it.
  • A failed emission removes that incident before raising an exception.
  • A successful emission records the existing incidentCreated business log.
  • Save IncidentService.java.
  • Restart IncidentApiApplication.

You should see the application start successfully with the updated creation path.

  • Repeat the valid POST request from the previous step in the PowerShell window from earlier.

You should receive incident JSON with a generated ID. The IntelliJ IDEA console should show the incidentCreated business event.

POST request failing now?

Compare the emission block with the code above. The emitResult check belongs after the list insertion and before the business log.

If the request still fails, help me trace the incident publication path.

The controller needs a publisher view that clients can subscribe to. The sink keeps ownership of emission inside the service while stream() exposes only its Flux view.

  • Add this stream() method below finiteFeed() in IncidentService.java:
    public Flux<Incident> stream() {
        return this.incidentStream.asFlux();
    }

Why expose a Flux view?

The asFlux() call gives subscribers a read-only publisher view. Incident emission remains controlled by IncidentService.

  • Save IncidentService.java.
  • Restart IncidentApiApplication.

You should see Started IncidentApiApplication with no compilation errors. The service now owns a replaying stream of seeded plus newly created incidents.

Stream method not compiling?

Confirm that stream() is inside IncidentService. Check that its return type is Flux<Incident>.

For help finding a misplaced brace, help me inspect the service structure.

✔️ Awesome, I've got everything!

Great. Your service now publishes seed data plus successful incident creations through one bounded replaying sink.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.service;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.dto.CreateIncidentRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;

@Service
public class IncidentService {
    private static final Logger log = LoggerFactory.getLogger(IncidentService.class);
    private final List<Incident> incidents = new CopyOnWriteArrayList<>();
    private final Sinks.Many<Incident> incidentStream = Sinks.many().replay().limit(20);

    public IncidentService() {
        seed("Checkout API latency", "HIGH");
        seed("Email delivery delay", "MEDIUM");
        seed("Metrics exporter offline", "CRITICAL");
    }

    public Flux<Incident> findAll(Optional<String> severity) {
        Flux<Incident> source = Flux.defer(() -> Flux.fromIterable(List.copyOf(this.incidents)));
        return severity
                .filter(value -> !value.isBlank())
                .map(value -> source.filter(incident -> incident.severity().equalsIgnoreCase(value)))
                .orElse(source);
    }

    public Mono<Incident> create(CreateIncidentRequest request) {
        return Mono.fromSupplier(() -> {
            Incident incident = new Incident(
                    UUID.randomUUID(),
                    request.title(),
                    request.severity().toUpperCase(Locale.ROOT),
                    Instant.now()
            );
            this.incidents.add(incident);
            Sinks.EmitResult emitResult = this.incidentStream.tryEmitNext(incident);
            if (emitResult.isFailure()) {
                this.incidents.remove(incident);
                throw new IllegalStateException("Unable to publish incident: " + emitResult);
            }
            log.info("incidentCreated id={} severity={}", incident.id(), incident.severity());
            return incident;
        });
    }

    public Flux<Incident> finiteFeed() {
        return findAll(Optional.empty()).delayElements(Duration.ofSeconds(1));
    }

    public Flux<Incident> stream() {
        return this.incidentStream.asFlux();
    }

    private void seed(String title, String severity) {
        Incident incident = new Incident(UUID.randomUUID(), title, severity, Instant.now());
        this.incidents.add(incident);
        this.incidentStream.tryEmitNext(incident).orThrow();
    }
}

What should match?

Compare this reference with your file. Pay close attention to the sink field plus the emission logic in create() and seed().

Expose the Server-Sent Events endpoint

An SSE response tells the HTTP client to keep reading events from the returned Flux. The media type distinguishes this persistent stream from the finite JSON feed.

  • In IncidentController.java, replace the imports around jakarta.validation.Valid with this section:
import jakarta.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;

Why import MediaType?

The MediaType class provides the constant used to declare an SSE response. This keeps the mapping tied to Spring's supported media type definition.

  • Save IncidentController.java.
  • Restart IncidentApiApplication.

You should see the application start successfully with the new controller import.

The new route can now return the service's live publisher. Declaring MediaType.TEXT_EVENT_STREAM_VALUE keeps the HTTP connection open for future elements.

  • Add this endpoint below feed() in IncidentController.java:
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<Incident> stream() {
        return this.incidentService.stream();
    }

How does the endpoint stay live?

  • The mapping exposes /api/incidents/stream under the controller's existing base path.
  • The SSE media type tells WebFlux to write incidents as a continuing event stream.
  • The controller returns the Flux directly without calling block() or subscribe().
  • Save IncidentController.java.
  • Restart IncidentApiApplication.
  • Predict whether the connection closes after replaying the current incidents.
  • Start the SSE client in the PowerShell window from earlier by running this command:
curl.exe --no-buffer http://localhost:8080/api/incidents/stream

What does this command test?

The --no-buffer flag displays each event as curl receives it. The open command tests whether the publisher remains subscribed after replaying its history.

You should see the three seeded incidents arrive. The command stays attached to the open connection instead of returning to the prompt.

  • Switch to a second PowerShell window.
  • Repeat the valid POST request from the previous step.

The first PowerShell window should display the newly created incident after the replayed history. That live arrival proves the publisher can deliver events created after subscription.

  • Cancel the stream in the first PowerShell window.

Stream closing too early?

Confirm that the new controller method calls incidentService.stream(). Check that its mapping produces MediaType.TEXT_EVENT_STREAM_VALUE.

If new POST requests never reach the client, help me trace the SSE stream.

✔️ Awesome, I've got everything!

Excellent. Your controller now keeps an SSE connection open for replayed plus future incidents.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.controller;

import java.util.List;
import java.util.Optional;

import com.nextwork.incidents.domain.Incident;
import com.nextwork.incidents.dto.CreateIncidentRequest;
import com.nextwork.incidents.service.IncidentService;
import jakarta.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/incidents")
public class IncidentController {
    private final IncidentService incidentService;

    public IncidentController(IncidentService incidentService) {
        this.incidentService = incidentService;
    }

    @GetMapping
    public Mono<List<Incident>> list(@RequestParam(required = false) String severity) {
        return this.incidentService.findAll(Optional.ofNullable(severity)).collectList();
    }

    @PostMapping
    public Mono<Incident> create(@Valid @RequestBody CreateIncidentRequest request) {
        return this.incidentService.create(request);
    }

    @GetMapping("/feed")
    public Flux<Incident> feed() {
        return this.incidentService.finiteFeed();
    }

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<Incident> stream() {
        return this.incidentService.stream();
    }
}

What should match?

Compare the imports plus the final stream() method with your controller. The existing snapshot, creation, and finite feed mappings should remain unchanged.

Log completed and cancelled exchanges

A live stream may end through cancellation instead of normal completion. A WebFilter can attach one final callback to every exchange and record its terminal signal through SLF4J.

You will assemble the filter file in two adjacent parts. Add both parts before restarting the application.

  • Create RequestLoggingFilter.java inside the existing logging package with this top section:
package com.nextwork.incidents.logging;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

What does this section prepare?

  • The package places request logging inside the project's existing cross-cutting layer.
  • The Spring imports provide the exchange plus filter-chain contracts.
  • The Reactor import provides the asynchronous completion type returned by the filter.
  • Confirm that IntelliJ IDEA lists RequestLoggingFilter.java inside the logging package.

File in the wrong package?

Check that the file sits under src/main/java/com/nextwork/incidents/logging. Its package declaration must match that folder path.

If IntelliJ IDEA shows a package mismatch, help me fix the filter location.

The filter records its start time before continuing the chain. Its final callback calculates elapsed milliseconds and logs the exchange details after completion or cancellation.

  • Complete RequestLoggingFilter.java by adding this class below the imports:
@Component
public class RequestLoggingFilter implements WebFilter {
    private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        long startedAt = System.nanoTime();
        return chain.filter(exchange)
                .doFinally(signalType -> {
                    long durationMs = (System.nanoTime() - startedAt) / 1_000_000;
                    log.info(
                            "{}method={} path={} status={} durationMs={} signal={}",
                            exchange.getLogPrefix(),
                            exchange.getRequest().getMethod(),
                            exchange.getRequest().getURI().getPath(),
                            exchange.getResponse().getStatusCode(),
                            durationMs,
                            signalType
                    );
                });
    }
}

How does request logging work?

  • The @Component annotation lets Spring discover the filter automatically.
  • The filter continues the existing chain without blocking it.
  • The doFinally callback runs for completion, error, or cancellation.
  • Each log entry includes the exchange prefix, HTTP method, path, status, duration, and terminal signal.
  • Save RequestLoggingFilter.java.
  • Restart IncidentApiApplication.

You should see Started IncidentApiApplication in the console. Spring has discovered the filter without any manual registration.

Filter not compiling?

Check the WebFilter method signature plus the closing braces around doFinally. Confirm that every imported type appears in the class body.

For help comparing the structure, help me debug the request filter.

✔️ Awesome, I've got everything!

Your request filter is ready to record normal completions plus cancelled stream subscriptions.

ⓧ I'd like to double check the full code

package com.nextwork.incidents.logging;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Component
public class RequestLoggingFilter implements WebFilter {
    private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        long startedAt = System.nanoTime();
        return chain.filter(exchange)
                .doFinally(signalType -> {
                    long durationMs = (System.nanoTime() - startedAt) / 1_000_000;
                    log.info(
                            "{}method={} path={} status={} durationMs={} signal={}",
                            exchange.getLogPrefix(),
                            exchange.getRequest().getMethod(),
                            exchange.getRequest().getURI().getPath(),
                            exchange.getResponse().getStatusCode(),
                            durationMs,
                            signalType
                    );
                });
    }
}

What should match?

Compare the complete filter with your file. The final log statement should receive all six values in the same order as its placeholders.

  • Predict what the first PowerShell window does after another incident is created.
  • Start the final live-stream check by running this command:
curl.exe --no-buffer http://localhost:8080/api/incidents/stream

What does the final check prove?

The open client verifies that replay still works after the filter was added. It also creates a long-running exchange whose cancellation can be recorded.

  • Switch to a second PowerShell window.
  • Repeat the valid POST request from the previous step.

You should see the new incident arrive in the first PowerShell window. The IntelliJ IDEA console should record the creation request plus its incidentCreated business event.

  • Cancel the stream in the first PowerShell window.
  • Return to the IntelliJ IDEA console.

You should see structured request entries containing the method, path, status, duration, and terminal signal. The stream entry should show that its exchange ended through cancellation.

Missing request logs?

Confirm that RequestLoggingFilter has @Component above the class declaration. Check that the application was restarted after the file was created.

If business logs appear without request logs, help me verify filter discovery.

That is the reactive loop working end to end. Your microservice now replays incident history, pushes future incidents over SSE, and records how every HTTP exchange finishes.

Secret mission

Filter the Live Incident Stream

Add an optional severity query parameter to the live stream. It filters replayed history plus future incidents. The SSE connection stays open.

Clean Up Your Resources

Clean Up Your Resources

Your service runs entirely on your Windows machine with no ongoing cost. Choose to keep it available or stop it for later or delete its files.

Resources you used:

  • Your running IncidentApiApplication process in IntelliJ IDEA on port 8080.
  • Your live Server-Sent Events client sessions in Windows PowerShell.
  • Your local incident-service project folder.

Keep everything running

No action needed. Choose this if you are still testing snapshots or live incident streams.

  • Leave IncidentApiApplication running in IntelliJ IDEA.
  • Keep the incident-service folder on your Windows machine.
  • Return to your existing PowerShell windows when you want to test the stream again.

Pause - I'll come back to this later

Pausing frees port 8080 for another application. Your source files stay inside incident-service.

  • Return to each PowerShell window that contains a live stream.
  • Press Ctrl+C to end each stream.
  • Switch back to the IntelliJ IDEA run console from earlier.
  • Use IntelliJ IDEA's running-process control to stop IncidentApiApplication.

That's a clean pause. Each PowerShell window returns to its prompt.

IntelliJ IDEA no longer shows an active IncidentApiApplication process.

Delete - I don't want to use this again

Deleting the project folder is permanent. Your installed development tools stay untouched.

  • Locate incident-service in IntelliJ IDEA's project sidebar.
  • Use IntelliJ IDEA's file-location action to reveal incident-service in File Explorer.
  • Copy the folder's full path into path to your incident-service folder.
  • Switch back to the IntelliJ IDEA run console from earlier.
  • Use IntelliJ IDEA's running-process control to stop IncidentApiApplication.
  • Close IntelliJ IDEA.

The service is now stopped. Its open stream connections return to their PowerShell prompts.

  • Press the Windows key to open Windows search.
  • Search for Command Prompt.
  • Select Command Prompt from the search results.
  • Remove the project folder by running this command:
rmdir /s /q "[[INCIDENT_SERVICE_PATH="path to your incident-service folder"]]"

What does this cleanup command do?

The command deletes the selected incident-service folder plus every file inside it. Quiet mode skips an extra confirmation prompt.

  • Return to the parent folder in File Explorer to confirm the deletion.

Your local workspace is clean. The incident-service folder is no longer listed.

Folder still present?

If Windows reports that the folder is in use, close any remaining terminals that are using it. Run the cleanup command again.

If Windows cannot find the path, reveal the folder in File Explorer again. Copy its full path without adding another folder name.

Help me remove the incident-service folder safely.

Nice Work!

Nice Work!

Strong finish! You built a focused incident-management microservice with Spring WebFlux.

What you learned:

  • Built annotated Spring WebFlux endpoints that return Mono snapshots plus Flux feeds. Used constructor injection to connect IncidentController with IncidentService.
  • Filtered incident snapshots through a severity query parameter. Validated new incidents with Jakarta Validation. Invalid request bodies now receive HTTP 400 responses.
  • Exposed the limit of a finite publisher when the feed completed. Replaced that limitation with a live Server-Sent Events stream backed by a replaying Project Reactor sink. Added structured logs for business events plus completed or cancelled HTTP exchanges.
  • Secret Mission: Added server-side severity filtering to the live stream. Matching replayed history plus matching future incidents now reach each subscriber.

Ready to quiz yourself?