Real-Time Endpoint Health Monitor
Build a Spring Boot service that monitors HTTP endpoints and pushes live status updates.
Introduction
30 Second Summary
Every time you visit a website, there's a backend service somewhere checking whether that site is actually online. When something goes down, engineering teams need to know instantly. The tools that make this possible are called health monitors.
In this project, you will build a real-time endpoint health monitoring service using Spring Boot. Your service will periodically check configured URLs, record their response times, and push live status updates to a browser dashboard.
What You'll Build
A backend monitoring service that pings HTTP endpoints on a schedule, persists the results in a database, exposes the data through a REST API, and broadcasts real-time updates to connected browsers via WebSocket.
By the end of this project, you'll have:
- A Spring Boot application with REST endpoints for managing monitored targets and querying check history.
- A scheduled health-check engine that measures HTTP response time and status on a 30-second interval.
- A real-time dashboard that receives live updates over WebSocket without polling.
- Secret Mission: Add configurable alert thresholds that flag endpoints as degraded when response time exceeds a limit, with visual warnings on the dashboard.
Are there any prerequisites?
You should have basic Java knowledge (classes, interfaces, annotations) and familiarity with HTTP concepts like status codes and GET requests. No prior Spring Boot experience is required.
Generate the Spring Boot Project
Every monitoring tool, notification system, and real-time dashboard starts with the same foundation: a backend service that can schedule work, persist data, and push updates.
In this step, you will scaffold that foundation using Spring Initializr. You will configure the project with the exact dependencies needed for REST APIs, WebSocket communication, database persistence, and production health checks.
In this step, get ready to:
- Generate a Spring Boot project with all required dependencies.
- Download, extract, and open the project in VS Code.
- Verify the application starts and responds on port 8080.
Configure and generate the project on Spring Initializr
Spring Initializr is a web tool that generates pre-configured Spring Boot project skeletons. It bundles your chosen dependencies into a ready-to-run ZIP file.
- Open your browser and navigate to https://start.spring.io.
- Under Project, select Maven.
- Under Language, select Java.
- Under Spring Boot, select 3.5.14.
- In the Project Metadata section, set Group to com.example.
- Set Artifact to endpoint-monitor.
- Leave Packaging as Jar.
- Set Java to 17.
Now add the five dependencies your project needs.
- Click ADD DEPENDENCIES in the top right corner of the page.
- Search for and add Spring Web.
- Search for and add WebSocket.
- Search for and add Spring Data JPA.
- Search for and add H2 Database.
- Search for and add Spring Boot Actuator.
What are these dependencies for?
Spring Web provides the REST API framework. WebSocket enables real-time browser communication via the STOMP protocol.
Spring Data JPA handles database persistence. H2 Database gives you a lightweight in-memory database with zero setup.
Spring Boot Actuator adds production-grade health check endpoints for your own app.
- Click GENERATE at the bottom of the page to download the project ZIP.
Extract the ZIP and open the project in VS Code
- Locate the downloaded endpoint-monitor.zip file in your Downloads folder.
- Extract the ZIP file to your Desktop.
- Switch back to VS Code.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the endpoint-monitor folder, and click Open.
You should see the Spring Boot project structure in the VS Code Explorer sidebar, including folders like src, .mvn, and files like pom.xml.
What is the Maven Wrapper?
Inside your project you will see mvnw (macOS/Linux) and mvnw.cmd (Windows) files. These are the Maven Wrapper scripts.
The Maven Wrapper downloads and uses a specific Maven version for this project automatically. You do not need to install Maven separately.
Don't see the project structure?
Make sure you extracted the ZIP first. Opening the .zip file directly will not work. You need to extract it into a folder, then open that folder in VS Code.
help me find and open my extracted Spring Boot project in VS Code
Verify the application starts on port 8080
The generated project is already a runnable Spring Boot application. Let's confirm everything is wired correctly by starting it.
- Open a terminal in VS Code by pressing Ctrl+` (macOS and Windows).
- Start the application by running the command for your operating system:
macOS
./mvnw spring-boot:run
Permission denied?
If you see a permission error, make the wrapper executable first by running:
chmod +x mvnw
Then re-run the ./mvnw spring-boot:run command. Linux users should use this same tab.
Windows
mvnw.cmd spring-boot:run
What does this command do?
The Maven Wrapper downloads all project dependencies (this may take a minute on the first run), compiles the code, and starts the embedded Tomcat server on port 8080.
Wait for the terminal output to show a line containing Started EndpointMonitorApplication. This confirms the app is running.
- Open your browser and navigate to http://localhost:8080/actuator/health.
You should see a JSON response containing "status":"UP". This is the Spring Boot Actuator health endpoint confirming your application is running and healthy.
App won't start or port 8080 already in use?
If you see an error about port 8080 being in use, another process is occupying that port. Stop the other process or close other running servers before retrying.
If the build fails with a Java version error, confirm your JDK version is 17 or higher by running java -version in your terminal.
help me troubleshoot my Spring Boot application failing to start
✔️ Awesome, I've got everything!
Great. Make sure you leave the application running in your terminal for the next step.
ⓧ I'd like to double check the full code
At this point, the only file you may want to check is application.properties. Spring Initializr generates it with just one line:
spring.application.name=endpoint-monitor
Your Spring Boot skeleton is alive and responding. Next up, you will define the data model that represents monitored endpoints and their check results.
Define the Data Model and Repository
Your Spring Boot project is generated and running. Now you need to define what data the health monitor will store.
Every monitoring system needs two things: a list of targets to watch, and a history of check results. In this step, you will create JPA entities that represent these two concepts, then wire them up to Spring Data JPA repositories that handle database operations automatically.
In this step, get ready to:
- Create JPA entity classes for endpoints and check results.
- Create Spring Data JPA repository interfaces for both entities.
- Configure the H2 in-memory database and verify the schema is auto-created.
Create the Endpoint and CheckResult entities
JPA entities are Java classes that map directly to database tables. Each field becomes a column, and each instance becomes a row.
- In the VS Code Explorer sidebar, right-click on the src/main/java/com/example/endpointmonitor folder.
- Create a new folder called model.
- Inside the model folder, create a new file called Endpoint.java.
- Paste the following code into Endpoint.java:
package com.example.endpointmonitor.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Endpoint {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String url;
private String status;
public Endpoint() {}
public Endpoint(String name, String url) {
this.name = name;
this.url = url;
this.status = "UNKNOWN";
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
What does this code do?
The @Entity annotation tells JPA to create a database table from this class. Each field (id, name, url, status) becomes a column in that table.
The @Id and @GeneratedValue annotations mark id as the primary key and tell the database to auto-increment it. The no-argument constructor is required by JPA for creating instances from database rows.
- Save Endpoint.java.
You will not see a visible change yet. The database table gets created when we configure H2 in the final substep.
- Inside the same model folder, create a new file called CheckResult.java.
- Paste the following code into CheckResult.java:
package com.example.endpointmonitor.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import java.time.LocalDateTime;
@Entity
public class CheckResult {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Endpoint endpoint;
private int statusCode;
private long responseTimeMs;
private LocalDateTime checkedAt;
public CheckResult() {}
public CheckResult(Endpoint endpoint, int statusCode, long responseTimeMs) {
this.endpoint = endpoint;
this.statusCode = statusCode;
this.responseTimeMs = responseTimeMs;
this.checkedAt = LocalDateTime.now();
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Endpoint getEndpoint() { return endpoint; }
public void setEndpoint(Endpoint endpoint) { this.endpoint = endpoint; }
public int getStatusCode() { return statusCode; }
public void setStatusCode(int statusCode) { this.statusCode = statusCode; }
public long getResponseTimeMs() { return responseTimeMs; }
public void setResponseTimeMs(long responseTimeMs) { this.responseTimeMs = responseTimeMs; }
public LocalDateTime getCheckedAt() { return checkedAt; }
public void setCheckedAt(LocalDateTime checkedAt) { this.checkedAt = checkedAt; }
}
What does this code do?
The @ManyToOne annotation creates a foreign-key relationship. Many check results can belong to one endpoint. JPA automatically creates a join column in the CHECK_RESULT table pointing to the ENDPOINT table.
The parameterized constructor sets checkedAt to the current time automatically. This means each time you create a new check result, the timestamp records itself.
- Save CheckResult.java.
You will not see a visible change yet. The next substep wires these entities to Spring Data repositories.
Seeing red underlines in VS Code?
If you see errors related to jakarta.persistence imports, make sure your project downloaded its dependencies properly. Try restarting VS Code or running the Maven Wrapper command to force a dependency download.
help me fix jakarta.persistence import errors in my Spring Boot project
✔️ Awesome, I've got everything!
Great. Double check you have saved both files in the model folder.
ⓧ I'd like to double check the full code
Here is Endpoint.java:
package com.example.endpointmonitor.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Endpoint {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String url;
private String status;
public Endpoint() {}
public Endpoint(String name, String url) {
this.name = name;
this.url = url;
this.status = "UNKNOWN";
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
Here is CheckResult.java:
package com.example.endpointmonitor.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import java.time.LocalDateTime;
@Entity
public class CheckResult {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Endpoint endpoint;
private int statusCode;
private long responseTimeMs;
private LocalDateTime checkedAt;
public CheckResult() {}
public CheckResult(Endpoint endpoint, int statusCode, long responseTimeMs) {
this.endpoint = endpoint;
this.statusCode = statusCode;
this.responseTimeMs = responseTimeMs;
this.checkedAt = LocalDateTime.now();
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Endpoint getEndpoint() { return endpoint; }
public void setEndpoint(Endpoint endpoint) { this.endpoint = endpoint; }
public int getStatusCode() { return statusCode; }
public void setStatusCode(int statusCode) { this.statusCode = statusCode; }
public long getResponseTimeMs() { return responseTimeMs; }
public void setResponseTimeMs(long responseTimeMs) { this.responseTimeMs = responseTimeMs; }
public LocalDateTime getCheckedAt() { return checkedAt; }
public void setCheckedAt(LocalDateTime checkedAt) { this.checkedAt = checkedAt; }
}
Create the repository interfaces
Spring Data JPA repositories give you full database CRUD operations without writing SQL. You define an interface, and Spring generates the implementation at runtime.
- In the VS Code Explorer sidebar, right-click on the src/main/java/com/example/endpointmonitor folder.
- Create a new folder called repository.
- Inside the repository folder, create a new file called EndpointRepository.java.
- Paste the following code into EndpointRepository.java:
package com.example.endpointmonitor.repository;
import com.example.endpointmonitor.model.Endpoint;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EndpointRepository extends JpaRepository<Endpoint, Long> {
}
- Save EndpointRepository.java.
- Create a second file in the repository folder called CheckResultRepository.java.
- Paste the following code into CheckResultRepository.java:
package com.example.endpointmonitor.repository;
import com.example.endpointmonitor.model.CheckResult;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CheckResultRepository extends JpaRepository<CheckResult, Long> {
List<CheckResult> findTop10ByEndpointIdOrderByCheckedAtDesc(Long endpointId);
}
What do these interfaces do?
By extending JpaRepository, each interface automatically gets methods like findAll(), save(), deleteById(), and more. You do not need to write any implementation code.
The findTop10ByEndpointIdOrderByCheckedAtDesc method in CheckResultRepository is a Spring Data query method. Spring parses the method name and generates a SQL query that finds the 10 most recent check results for a given endpoint, ordered newest first.
- Save CheckResultRepository.java.
You will not see a visible change yet. The repositories become active once the database is configured in the next substep.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files in the repository folder.
ⓧ I'd like to double check the full code
Here is EndpointRepository.java:
package com.example.endpointmonitor.repository;
import com.example.endpointmonitor.model.Endpoint;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EndpointRepository extends JpaRepository<Endpoint, Long> {
}
Here is CheckResultRepository.java:
package com.example.endpointmonitor.repository;
import com.example.endpointmonitor.model.CheckResult;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CheckResultRepository extends JpaRepository<CheckResult, Long> {
List<CheckResult> findTop10ByEndpointIdOrderByCheckedAtDesc(Long endpointId);
}
Configure the H2 database and verify the schema
H2 is an in-memory database. It lives entirely in your application's memory, so there is nothing to install. You just need to tell Spring Boot how to connect to it.
- In the VS Code Explorer sidebar, expand src/main/resources and open application.properties.
- Replace the entire contents of the file with the following configuration:
spring.application.name=endpoint-monitor
# H2 Database
spring.datasource.url=jdbc:h2:mem:monitordb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Actuator
management.endpoints.web.exposure.include=health,info
What do these properties do?
The spring.datasource.url sets up a named in-memory database called monitordb. The ddl-auto=create-drop setting tells JPA to create tables from your entity classes on startup and drop them on shutdown. This is perfect for development.
The spring.h2.console.enabled=true activates a web-based database browser at /h2-console. The Actuator line exposes the /actuator/health and /actuator/info endpoints for monitoring your own app.
- Save application.properties.
- If your app is still running from Step 1, stop it by pressing Ctrl+C in the terminal.
- Restart the application by running:
macOS
./mvnw spring-boot:run
Windows
mvnw.cmd spring-boot:run
- Wait for the console to show Started EndpointMonitorApplication and then open your browser.
- Navigate to http://localhost:8080/h2-console.
- In the JDBC URL field, enter jdbc:h2:mem:monitordb.
- In the User Name field, enter sa.
- Leave the Password field empty.
- Click Connect.
You should see ENDPOINT and CHECK_RESULT tables listed in the left sidebar. This confirms JPA automatically created your database schema from the entity classes.
Don't see the tables in H2 console?
Make sure the JDBC URL is exactly jdbc:h2:mem:monitordb. If you leave the default value (which is usually jdbc:h2:~/test), you will connect to a different database that has no tables.
Also check that your app is still running. The in-memory database only exists while the application is active.
help me connect to the H2 console in my Spring Boot project
✔️ Awesome, I've got everything!
Great. Double check you have saved application.properties.
ⓧ I'd like to double check the full code
spring.application.name=endpoint-monitor
# H2 Database
spring.datasource.url=jdbc:h2:mem:monitordb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Actuator
management.endpoints.web.exposure.include=health,info
Your data model is in place and the database is ready. Next up, you will build a REST API that lets clients add, list, and remove monitored endpoints.
Build the REST API
Your data model and repositories are ready. Now you need a way for the outside world to interact with them.
In this step, you will create REST API controllers that expose HTTP endpoints for adding, listing, and removing monitored targets. You will also expose a history endpoint so clients can query past check results.
In this step, get ready to:
- Create an EndpointController with CRUD operations for monitored endpoints.
- Create a CheckResultController that returns check history.
- Test the API with curl to confirm everything works.
Create the endpoint controller
A REST controller is the layer that receives incoming HTTP requests and returns responses. Spring Boot uses the @RestController annotation to mark a class as a controller that automatically serialises return values to JSON.
- In the VS Code Explorer sidebar, right-click the com.example.endpointmonitor package inside src/main/java and create a new folder called controller.
- Inside the controller folder, create a new file called EndpointController.java.
- Paste the following code into EndpointController.java:
package com.example.endpointmonitor.controller;
import com.example.endpointmonitor.model.Endpoint;
import com.example.endpointmonitor.repository.EndpointRepository;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/endpoints")
public class EndpointController {
// Repository for accessing endpoint data in the database
private final EndpointRepository endpointRepository;
// Constructor injection — Spring automatically provides the repository
public EndpointController(EndpointRepository endpointRepository) {
this.endpointRepository = endpointRepository;
}
// GET /api/endpoints — returns all registered endpoints
@GetMapping
public List<Endpoint> getAll() {
return endpointRepository.findAll();
}
// POST /api/endpoints — creates a new endpoint to monitor
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Endpoint create(@RequestBody Endpoint endpoint) {
endpoint.setStatus("UNKNOWN");
return endpointRepository.save(endpoint);
}
// DELETE /api/endpoints/{id} — removes an endpoint by its ID
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
endpointRepository.deleteById(id);
}
}
What does this code do?
This controller maps three HTTP operations to your Endpoint database table:
- GET /api/endpoints fetches every endpoint you have registered.
- POST /api/endpoints accepts a JSON body with name and url fields, sets the status to UNKNOWN, and saves it. It returns HTTP 201 (Created).
- DELETE /api/endpoints/{id} removes the endpoint matching that ID and returns HTTP 204 (No Content).
- Save EndpointController.java.
Create the check result controller
Clients will also need to query the history of health checks for a specific endpoint. This second controller exposes a single read-only route for that purpose.
- Inside the controller folder, create a new file called CheckResultController.java.
- Paste the following code into CheckResultController.java:
package com.example.endpointmonitor.controller;
import com.example.endpointmonitor.model.CheckResult;
import com.example.endpointmonitor.repository.CheckResultRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/endpoints")
public class CheckResultController {
// Repository for querying check result history
private final CheckResultRepository checkResultRepository;
public CheckResultController(CheckResultRepository checkResultRepository) {
this.checkResultRepository = checkResultRepository;
}
// GET /api/endpoints/{id}/history — returns the last 10 check results
@GetMapping("/{id}/history")
public List<CheckResult> getHistory(@PathVariable Long id) {
return checkResultRepository.findTop10ByEndpointIdOrderByCheckedAtDesc(id);
}
}
What does this code do?
This controller exposes GET /api/endpoints/{id}/history. It calls the custom query method you defined earlier in CheckResultRepository to return the 10 most recent check results for a given endpoint, ordered newest-first.
- Save CheckResultController.java.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the full EndpointController.java:
package com.example.endpointmonitor.controller;
import com.example.endpointmonitor.model.Endpoint;
import com.example.endpointmonitor.repository.EndpointRepository;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/endpoints")
public class EndpointController {
private final EndpointRepository endpointRepository;
public EndpointController(EndpointRepository endpointRepository) {
this.endpointRepository = endpointRepository;
}
@GetMapping
public List<Endpoint> getAll() {
return endpointRepository.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Endpoint create(@RequestBody Endpoint endpoint) {
endpoint.setStatus("UNKNOWN");
return endpointRepository.save(endpoint);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
endpointRepository.deleteById(id);
}
}
And the full CheckResultController.java:
package com.example.endpointmonitor.controller;
import com.example.endpointmonitor.model.CheckResult;
import com.example.endpointmonitor.repository.CheckResultRepository;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/endpoints")
public class CheckResultController {
private final CheckResultRepository checkResultRepository;
public CheckResultController(CheckResultRepository checkResultRepository) {
this.checkResultRepository = checkResultRepository;
}
@GetMapping("/{id}/history")
public List<CheckResult> getHistory(@PathVariable Long id) {
return checkResultRepository.findTop10ByEndpointIdOrderByCheckedAtDesc(id);
}
}
Test the API with curl
Your controllers are in place but you have not verified they actually work. You will use curl to send HTTP requests from the terminal and confirm the API responds correctly.
- Make sure the application is running. If you stopped it earlier, start it again by running:
mvnw.cmd spring-boot:run
- Open a second terminal in VS Code by clicking **Terminal** in the top menu bar, then **New Terminal**.
- Add a test endpoint that returns HTTP 200 by running this command:
macOS
curl -X POST http://localhost:8080/api/endpoints -H "Content-Type: application/json" -d '{"name": "HTTP 200 Test", "url": "https://httpstat.us/200"}'
Windows
curl -X POST http://localhost:8080/api/endpoints -H "Content-Type: application/json" -d "{\"name\": \"HTTP 200 Test\", \"url\": \"https://httpstat.us/200\"}"
What is httpstat.us?
httpstat.us is a free public service that returns whatever HTTP status code you put in the URL. It is perfect for testing because you can simulate both healthy and unhealthy endpoints without setting up real services.
You should see a JSON response containing the saved endpoint with an id, name, url, and a status of UNKNOWN.
- Add a second endpoint that returns HTTP 500 by running:
macOS
curl -X POST http://localhost:8080/api/endpoints -H "Content-Type: application/json" -d '{"name": "HTTP 500 Test", "url": "https://httpstat.us/500"}'
Windows
curl -X POST http://localhost:8080/api/endpoints -H "Content-Type: application/json" -d "{\"name\": \"HTTP 500 Test\", \"url\": \"https://httpstat.us/500\"}"
- List all registered endpoints by running:
curl http://localhost:8080/api/endpoints
You should see a JSON array containing both endpoints you just created.
Getting an empty array or an error?
- Make sure the application is still running in the first terminal. Look for the Spring Boot banner and a line showing Started EndpointMonitorApplication.
- Check that you are sending the POST requests to http://localhost:8080/api/endpoints (not https).
- On Windows, make sure you escape inner quotes with a backslash as shown in the Windows tab.
help me debug my curl request to the Spring Boot REST API
Your REST API is live and accepting requests. Next up, you will build the scheduled health checker that automatically pings these endpoints and records whether they are up or down.
Implement the Scheduled Health Checker
Your REST API can store and retrieve endpoint data, but nothing is actually checking those endpoints yet.
In this step, you will build the engine that makes your monitor come alive. A scheduled service will automatically ping every registered URL on a timer, measure the response time, and persist the results. This is the same pattern that powers production monitoring tools like Datadog and Pingdom.
In this step, get ready to:
- Create a HealthCheckService that performs HTTP health checks on a schedule.
- Enable scheduling on the main application class.
- Verify periodic checks are running and persisting results.
Create the HealthCheckService
The health checker needs to do three things on a repeating timer: hit each endpoint with an HTTP GET, record how long the response takes, and save the result. Spring's @Scheduled annotation handles the timer, and RestClient handles the HTTP calls.
- In the VS Code Explorer sidebar, right-click the com.example.endpointmonitor package inside src/main/java.
- Create a new folder called service.
- Inside the service folder, create a new file called HealthCheckService.java.
- Add the full service class by pasting this code:
package com.example.endpointmonitor.service;
import com.example.endpointmonitor.model.CheckResult;
import com.example.endpointmonitor.model.Endpoint;
import com.example.endpointmonitor.repository.CheckResultRepository;
import com.example.endpointmonitor.repository.EndpointRepository;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class HealthCheckService {
private final EndpointRepository endpointRepository;
private final CheckResultRepository checkResultRepository;
private final SimpMessagingTemplate messagingTemplate;
private final RestClient restClient;
public HealthCheckService(EndpointRepository endpointRepository,
CheckResultRepository checkResultRepository,
SimpMessagingTemplate messagingTemplate) {
this.endpointRepository = endpointRepository;
this.checkResultRepository = checkResultRepository;
this.messagingTemplate = messagingTemplate;
this.restClient = RestClient.create();
}
// Runs every 30 seconds to check all registered endpoints
@Scheduled(fixedRate = 30000)
public void checkAllEndpoints() {
List<Endpoint> endpoints = endpointRepository.findAll();
for (Endpoint endpoint : endpoints) {
CheckResult result = performCheck(endpoint);
checkResultRepository.save(result);
// Update the endpoint's status based on the HTTP response code
endpoint.setStatus(result.getStatusCode() >= 200 && result.getStatusCode() < 300 ? "UP" : "DOWN");
endpointRepository.save(endpoint);
// Build a message payload for real-time updates
Map<String, Object> message = new HashMap<>();
message.put("endpointId", endpoint.getId());
message.put("name", endpoint.getName());
message.put("url", endpoint.getUrl());
message.put("status", endpoint.getStatus());
message.put("statusCode", result.getStatusCode());
message.put("responseTimeMs", result.getResponseTimeMs());
message.put("checkedAt", result.getCheckedAt().toString());
// Send the result to any WebSocket subscribers
messagingTemplate.convertAndSend("/topic/health-updates", message);
}
}
// Performs a single HTTP GET and measures response time
private CheckResult performCheck(Endpoint endpoint) {
long start = System.currentTimeMillis();
int statusCode;
try {
statusCode = restClient.get()
.uri(endpoint.getUrl())
.retrieve()
.toBodilessEntity()
.getStatusCode()
.value();
} catch (Exception e) {
statusCode = 0;
}
long responseTime = System.currentTimeMillis() - start;
return new CheckResult(endpoint, statusCode, responseTime);
}
}
What does this code do?
This service is the core engine of your health monitor. Here is what each piece does:
- @Service registers this class as a Spring-managed bean so it can receive injected dependencies.
- The constructor injects both repositories, a SimpMessagingTemplate for broadcasting real-time updates, and creates a RestClient instance for making HTTP requests.
- @Scheduled(fixedRate = 30000) tells Spring to call checkAllEndpoints() every 30 seconds automatically.
- performCheck() fires an HTTP GET, catches exceptions (returning status code 0 for unreachable hosts), and measures the round-trip time in milliseconds.
- After each check, the service updates the endpoint status to UP or DOWN and publishes the result to /topic/health-updates for WebSocket subscribers.
- Save HealthCheckService.java.
Why RestClient instead of RestTemplate?
Spring Boot 3.2+ introduced RestClient as the modern replacement for the older RestTemplate. It provides a cleaner fluent API and is the recommended approach for new projects.
Enable scheduling on the application class
Spring does not run @Scheduled methods unless you explicitly enable the scheduling infrastructure on a configuration class.
- Open EndpointMonitorApplication.java in the src/main/java/com/example/endpointmonitor folder.
- Find this line:
@SpringBootApplication
public class EndpointMonitorApplication {
- Add the @EnableScheduling annotation and its import so the file looks like this:
package com.example.endpointmonitor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class EndpointMonitorApplication {
public static void main(String[] args) {
SpringApplication.run(EndpointMonitorApplication.class, args);
}
}
What does @EnableScheduling do?
This annotation activates Spring's task scheduling infrastructure. Without it, any @Scheduled annotations in your project are silently ignored.
- Save EndpointMonitorApplication.java.
Run the app and verify health checks are executing
With the service created and scheduling enabled, your app will start pinging every registered endpoint every 30 seconds as soon as it boots.
- Stop the application if it is still running by pressing Ctrl+C in the terminal.
- Restart the application by running:
mvnw.cmd spring-boot:run
- The H2 in-memory database resets on every restart, so re-add a test endpoint by opening a second terminal and running:
curl -X POST http://localhost:8080/api/endpoints -H "Content-Type: application/json" -d "{\"name\":\"Test\",\"url\":\"https://httpstat.us/200\"}"
- Wait approximately 30 seconds for the first scheduled check cycle to execute.
- Query the history endpoint for the test endpoint you just added by running:
curl http://localhost:8080/api/endpoints/1/history
- You should see a JSON array containing check results with statusCode, responseTimeMs, and checkedAt fields populated.
What should I see?
The response will look something like this:
[
{
"id": 1,
"statusCode": 200,
"responseTimeMs": 342,
"checkedAt": "2026-06-05T10:30:15"
}
]
If you also add a test endpoint pointing to https://httpstat.us/500, its results will show statusCode: 500 confirming your monitor correctly records failures too.
Not seeing any results?
- Confirm you re-added a test endpoint after restarting. The H2 in-memory database clears on each restart.
- Wait a full 30 seconds after adding the endpoint before querying the history. The first check cycle needs time to complete.
- Check that @EnableScheduling is present on EndpointMonitorApplication.java. Without it, the scheduled method never fires.
- Verify your HealthCheckService.java file is saved inside the service package folder. If it is in the wrong location, Spring will not detect the @Service annotation.
help me debug why my scheduled health checks are not running
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete HealthCheckService.java:
package com.example.endpointmonitor.service;
import com.example.endpointmonitor.model.CheckResult;
import com.example.endpointmonitor.model.Endpoint;
import com.example.endpointmonitor.repository.CheckResultRepository;
import com.example.endpointmonitor.repository.EndpointRepository;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class HealthCheckService {
private final EndpointRepository endpointRepository;
private final CheckResultRepository checkResultRepository;
private final SimpMessagingTemplate messagingTemplate;
private final RestClient restClient;
public HealthCheckService(EndpointRepository endpointRepository,
CheckResultRepository checkResultRepository,
SimpMessagingTemplate messagingTemplate) {
this.endpointRepository = endpointRepository;
this.checkResultRepository = checkResultRepository;
this.messagingTemplate = messagingTemplate;
this.restClient = RestClient.create();
}
@Scheduled(fixedRate = 30000)
public void checkAllEndpoints() {
List<Endpoint> endpoints = endpointRepository.findAll();
for (Endpoint endpoint : endpoints) {
CheckResult result = performCheck(endpoint);
checkResultRepository.save(result);
endpoint.setStatus(result.getStatusCode() >= 200 && result.getStatusCode() < 300 ? "UP" : "DOWN");
endpointRepository.save(endpoint);
Map<String, Object> message = new HashMap<>();
message.put("endpointId", endpoint.getId());
message.put("name", endpoint.getName());
message.put("url", endpoint.getUrl());
message.put("status", endpoint.getStatus());
message.put("statusCode", result.getStatusCode());
message.put("responseTimeMs", result.getResponseTimeMs());
message.put("checkedAt", result.getCheckedAt().toString());
messagingTemplate.convertAndSend("/topic/health-updates", message);
}
}
private CheckResult performCheck(Endpoint endpoint) {
long start = System.currentTimeMillis();
int statusCode;
try {
statusCode = restClient.get()
.uri(endpoint.getUrl())
.retrieve()
.toBodilessEntity()
.getStatusCode()
.value();
} catch (Exception e) {
statusCode = 0;
}
long responseTime = System.currentTimeMillis() - start;
return new CheckResult(endpoint, statusCode, responseTime);
}
}
And here is the complete EndpointMonitorApplication.java:
package com.example.endpointmonitor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class EndpointMonitorApplication {
public static void main(String[] args) {
SpringApplication.run(EndpointMonitorApplication.class, args);
}
}
Your health monitor now runs autonomously, checking every registered endpoint on a 30-second cycle. Next up, you will connect browser clients to these updates in real-time using WebSocket.
Add Real-Time WebSocket Updates
Your health checker is saving results to the database and updating endpoint statuses. But right now, a browser client would need to repeatedly poll the REST API to see changes.
In this step, you'll configure WebSocket with the STOMP protocol so the server pushes health check results to connected browsers the instant they happen. Then you'll build a live dashboard that renders those updates in real-time.
In this step, get ready to:
- Configure the WebSocket message broker with STOMP.
- Connect the health check results to WebSocket publishing.
- Build a browser dashboard that receives live status updates.
Create the WebSocket configuration class
Spring Boot's WebSocket support needs a configuration class that defines how clients connect and how messages are routed. Without this, the SimpMessagingTemplate in your HealthCheckService cannot be autowired by Spring.
- In the VS Code Explorer sidebar, right-click the src/main/java/com/example/endpointmonitor folder.
- Select New Folder and name it config.
- Right-click the new config folder, select New File, and name it WebSocketConfig.java.
- Paste the following code into WebSocketConfig.java:
package com.example.endpointmonitor.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// Enable a simple in-memory message broker for topics
config.enableSimpleBroker("/topic");
// Set prefix for messages sent from the client to the server
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// Register the STOMP endpoint that clients connect to
registry.addEndpoint("/ws").setAllowedOriginPatterns("*");
}
}
What does this code do?
- @EnableWebSocketMessageBroker activates Spring's STOMP-over-WebSocket support and makes SimpMessagingTemplate available as a bean for dependency injection.
- enableSimpleBroker("/topic") creates an in-memory message broker that routes messages to clients subscribed to any destination starting with /topic.
- addEndpoint("/ws") registers the URL where browser clients open a WebSocket connection.
- setAllowedOriginPatterns("*") permits connections from any origin during development.
- Save WebSocketConfig.java.
Why STOMP instead of raw WebSocket?
Raw WebSocket gives you a bidirectional pipe, but you have to invent your own message format and routing. STOMP adds a lightweight publish-subscribe layer on top. Clients subscribe to named destinations (like /topic/health-updates) and the broker handles delivery.
This is the same pattern behind every real-time notification system and live dashboard in production.
Seeing a compile error?
- Make sure the file is inside the config folder, not alongside it. The package declaration must be com.example.endpointmonitor.config.
- Check that you have the full list of imports at the top. Missing an import for MessageBrokerRegistry or StompEndpointRegistry will cause errors.
Help me debug my WebSocketConfig compile error
Verify WebSocket publishing in HealthCheckService
Your HealthCheckService already injects SimpMessagingTemplate and publishes results after each check. Now that the WebSocket configuration exists, Spring can successfully provide that dependency.
- Open HealthCheckService.java and find the following line inside checkAllEndpoints():
messagingTemplate.convertAndSend("/topic/health-updates", message);
How does this connect to WebSocketConfig?
SimpMessagingTemplate is the server-side tool for sending messages to STOMP destinations. When you call convertAndSend("/topic/health-updates", message), the message broker you configured routes that message to every client subscribed to /topic/health-updates.
Each time the scheduled health check runs, every connected browser instantly receives the latest status without needing to poll.
No changes needed here. The publishing code is already in place from the previous step.
Create the live dashboard
Spring Boot serves static files from src/main/resources/static/ automatically. You'll create an HTML page that opens a WebSocket connection, subscribes to the health updates topic, and renders results in a live table.
- In the VS Code Explorer sidebar, right-click the src/main/resources/static folder.
- Select New File and name it index.html.
- Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Endpoint Health Monitor</title>
<!-- StompJS library for STOMP-over-WebSocket communication -->
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7/bundles/stomp.umd.min.js"></script>
<style>
body { font-family: sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #f5f5f5; }
.status-up { color: #28a745; font-weight: bold; }
.status-down { color: #dc3545; font-weight: bold; }
.status-unknown { color: #6c757d; font-weight: bold; }
#connection-status { padding: 5px 10px; border-radius: 4px; display: inline-block; margin-bottom: 10px; }
.connected { background-color: #d4edda; color: #155724; }
.disconnected { background-color: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>Endpoint Health Monitor</h1>
<!-- Connection indicator shows whether WebSocket is active -->
<div id="connection-status" class="disconnected">Disconnected</div>
<table>
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th>Status</th>
<th>Response Time</th>
<th>Last Checked</th>
</tr>
</thead>
<tbody id="results-body">
<tr><td colspan="5">Waiting for data...</td></tr>
</tbody>
</table>
<script>
// Store latest result per endpoint by ID
const endpoints = {};
// Create a STOMP client pointing at our WebSocket endpoint
const stompClient = new StompJs.Client({
brokerURL: 'ws://localhost:8080/ws'
});
// When connected, subscribe to health updates
stompClient.onConnect = function (frame) {
document.getElementById('connection-status').textContent = 'Connected';
document.getElementById('connection-status').className = 'connected';
stompClient.subscribe('/topic/health-updates', function (message) {
const data = JSON.parse(message.body);
endpoints[data.endpointId] = data;
renderTable();
});
};
// Update status indicator on disconnect
stompClient.onDisconnect = function () {
document.getElementById('connection-status').textContent = 'Disconnected';
document.getElementById('connection-status').className = 'disconnected';
};
// Open the WebSocket connection
stompClient.activate();
// Re-render the table with all known endpoint data
function renderTable() {
const tbody = document.getElementById('results-body');
const entries = Object.values(endpoints);
if (entries.length === 0) {
tbody.innerHTML = '<tr><td colspan="5">Waiting for data...</td></tr>';
return;
}
tbody.innerHTML = entries.map(function (ep) {
const statusClass = ep.status === 'UP' ? 'status-up' : ep.status === 'DOWN' ? 'status-down' : 'status-unknown';
return '<tr>' +
'<td>' + ep.name + '</td>' +
'<td>' + ep.url + '</td>' +
'<td class="' + statusClass + '">' + ep.status + '</td>' +
'<td>' + ep.responseTimeMs + ' ms</td>' +
'<td>' + ep.checkedAt + '</td>' +
'</tr>';
}).join('');
}
</script>
</body>
</html>
What does this code do?
- The <script> tag loads StompJS from a CDN. This is the JavaScript library Spring recommends for STOMP-over-WebSocket in the browser.
- new StompJs.Client({ brokerURL: 'ws://localhost:8080/ws' }) creates a STOMP client that connects to the /ws endpoint you registered in WebSocketConfig.
- stompClient.subscribe('/topic/health-updates', ...) listens for messages published by your HealthCheckService. Each incoming message triggers a table re-render.
- The endpoints object stores the latest result per endpoint ID, so the table always shows the most recent status for each monitored URL.
- Save index.html.
- Stop the running app if it's still active (press Ctrl+C in the terminal).
- Restart the application by running:
mvnw.cmd spring-boot:run
- Open your browser and navigate to http://localhost:8080.
You should see the "Endpoint Health Monitor" heading, a green "Connected" badge, and a table showing "Waiting for data..." in the body.
Why does it say Connected but Waiting for data?
The WebSocket connection is open and the subscription is active. But you haven't registered any endpoints to monitor yet. Once you add endpoints via the REST API, the scheduled health checker will run every 30 seconds and push results to this page automatically.
Seeing Disconnected or a blank page?
- Check the terminal for startup errors. If you see a message about failing to autowire SimpMessagingTemplate, verify that WebSocketConfig.java is in the correct package path and has the @EnableWebSocketMessageBroker annotation.
- Make sure index.html is inside src/main/resources/static/. Files outside this folder are not served by Spring Boot.
- Open your browser's developer console (F12) and look for WebSocket connection errors. A refused connection means the app is not running on port 8080.
Help me debug my WebSocket connection issue
✔️ Awesome, I've got everything!
Great. Double check that you've saved both WebSocketConfig.java and index.html, and that your dashboard shows "Connected".
ⓧ I'd like to double check the full code
Here is the full WebSocketConfig.java:
package com.example.endpointmonitor.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOriginPatterns("*");
}
}
Here is the full index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Endpoint Health Monitor</title>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7/bundles/stomp.umd.min.js"></script>
<style>
body { font-family: sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #f5f5f5; }
.status-up { color: #28a745; font-weight: bold; }
.status-down { color: #dc3545; font-weight: bold; }
.status-unknown { color: #6c757d; font-weight: bold; }
#connection-status { padding: 5px 10px; border-radius: 4px; display: inline-block; margin-bottom: 10px; }
.connected { background-color: #d4edda; color: #155724; }
.disconnected { background-color: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>Endpoint Health Monitor</h1>
<div id="connection-status" class="disconnected">Disconnected</div>
<table>
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th>Status</th>
<th>Response Time</th>
<th>Last Checked</th>
</tr>
</thead>
<tbody id="results-body">
<tr><td colspan="5">Waiting for data...</td></tr>
</tbody>
</table>
<script>
const endpoints = {};
const stompClient = new StompJs.Client({
brokerURL: 'ws://localhost:8080/ws'
});
stompClient.onConnect = function (frame) {
document.getElementById('connection-status').textContent = 'Connected';
document.getElementById('connection-status').className = 'connected';
stompClient.subscribe('/topic/health-updates', function (message) {
const data = JSON.parse(message.body);
endpoints[data.endpointId] = data;
renderTable();
});
};
stompClient.onDisconnect = function () {
document.getElementById('connection-status').textContent = 'Disconnected';
document.getElementById('connection-status').className = 'disconnected';
};
stompClient.activate();
function renderTable() {
const tbody = document.getElementById('results-body');
const entries = Object.values(endpoints);
if (entries.length === 0) {
tbody.innerHTML = '<tr><td colspan="5">Waiting for data...</td></tr>';
return;
}
tbody.innerHTML = entries.map(function (ep) {
const statusClass = ep.status === 'UP' ? 'status-up' : ep.status === 'DOWN' ? 'status-down' : 'status-unknown';
return '<tr>' +
'<td>' + ep.name + '</td>' +
'<td>' + ep.url + '</td>' +
'<td class="' + statusClass + '">' + ep.status + '</td>' +
'<td>' + ep.responseTimeMs + ' ms</td>' +
'<td>' + ep.checkedAt + '</td>' +
'</tr>';
}).join('');
}
</script>
</body>
</html>
Your real-time pipeline is wired end-to-end. The health checker runs on a schedule, saves results to the database, and instantly pushes them to any connected browser. Next up, you'll put the complete system through its paces by adding live endpoints and watching the dashboard react.
Test the Complete System
You have all the pieces in place. Your REST API manages endpoints, your scheduled health checker pings them every 30 seconds, and your WebSocket connection broadcasts results to the browser in real-time.
Now it is time to run the full end-to-end flow. You will add both healthy and unhealthy endpoints, watch the dashboard light up with live status updates, and confirm that Spring Boot Actuator is reporting your own app's health.
In this step, get ready to:
- Open the live dashboard and confirm the WebSocket connection.
- Add test endpoints and watch real-time status updates appear.
- Verify Spring Boot Actuator is reporting your app's own health.
Open the dashboard and confirm the WebSocket connection
Your dashboard connects to the Spring Boot server via WebSocket the moment you load the page. A green "Connected" badge confirms the STOMP handshake succeeded.
- Open your browser and navigate to http://localhost:8080.
You should see the "Endpoint Health Monitor" heading, a green "Connected" badge at the top, and a table showing "Waiting for data..." in the body.
What does the Connected badge mean?
The badge proves that your browser successfully opened a WebSocket connection to ws://localhost:8080/ws and subscribed to the /topic/health-updates STOMP destination.
Any message published to that topic by your HealthCheckService will appear in the table instantly, with no polling or page refreshes needed.
Seeing a red Disconnected badge?
- Make sure your Spring Boot app is still running in the terminal. If you stopped it, restart it by running mvnw.cmd spring-boot:run again.
- Check that no other application is using port 8080. Look for a "port already in use" error in your terminal output.
- Verify your WebSocketConfig.java has setAllowedOriginPatterns("*") on the endpoint registration.
Why won't my WebSocket connect to the Spring Boot server?
Add test endpoints and watch real-time updates
To see the health checker in action, you need to register some target URLs. You will add one endpoint that always returns HTTP 200 (healthy) and another that always returns HTTP 500 (unhealthy).
- Open a new terminal in VS Code by pressing Ctrl+Shift+` (macOS and Windows).
- Add a healthy test endpoint by running this command:
curl -X POST http://localhost:8080/api/endpoints -H "Content-Type: application/json" -d "{\"name\": \"HTTP 200 Test\", \"url\": \"https://httpstat.us/200\"}"
What is httpstat.us?
httpstat.us is a free public service that returns whatever HTTP status code you put in the URL. Hitting https://httpstat.us/200 always returns 200 OK, and https://httpstat.us/500 always returns 500 Internal Server Error.
This makes it perfect for testing a monitoring tool without needing to set up your own servers.
You should see a JSON response with the endpoint's id, name, url, and a status of UNKNOWN (it has not been checked yet).
- Add an unhealthy test endpoint by running this command:
curl -X POST http://localhost:8080/api/endpoints -H "Content-Type: application/json" -d "{\"name\": \"HTTP 500 Test\", \"url\": \"https://httpstat.us/500\"}"
- Switch back to your browser with the dashboard at http://localhost:8080.
- Wait up to 30 seconds for the scheduled health checker to run.
You should see the table populate with two rows. "HTTP 200 Test" displays a green "UP" status, and "HTTP 500 Test" displays a red "DOWN" status. Both rows show the response time in milliseconds and a timestamp.
Windows users and curl quotes
If you are using PowerShell and the curl command fails with a JSON parsing error, try using the VS Code REST Client extension instead. Create a file called test.http and write your requests there.
Dashboard still showing Waiting for data?
- Check that your curl POST commands returned a JSON response (not an error). If they did, the endpoints are registered and the scheduler will pick them up within 30 seconds.
- Look at the terminal running your Spring Boot app. You should see log output every 30 seconds as the scheduled method runs. If there are no logs, confirm that @EnableScheduling is present on your EndpointMonitorApplication class.
- Make sure the connection badge says "Connected" (green). If it is red, the WebSocket is not established and updates cannot reach the browser.
My dashboard table is not updating with health check results. How do I debug this?
Verify Spring Boot Actuator reports your app's health
Your monitor checks other services, but what about your own app's health? Spring Boot Actuator provides a built-in health endpoint that production infrastructure (load balancers, container orchestrators) uses to know whether your service is alive.
- Check your app's own health by running this command in the terminal:
curl http://localhost:8080/actuator/health
You should see a JSON response showing {"status":"UP"}. This confirms your Spring Boot application and its database connection are both healthy.
Why does Actuator matter?
In production, tools like Kubernetes or AWS load balancers poll /actuator/health to decide if your service should receive traffic. If the endpoint returns anything other than UP, the orchestrator can restart the container or route traffic elsewhere.
You configured this in application.properties with management.endpoints.web.exposure.include=health,info. Without this line, Actuator endpoints are not exposed over HTTP.
- You can also visit http://localhost:8080/actuator/health directly in your browser to see the same JSON response.
Getting a 404 on /actuator/health?
- Open application.properties and confirm you have the line management.endpoints.web.exposure.include=health,info under the Actuator section.
- Make sure the spring-boot-starter-actuator dependency is present in your pom.xml. If you missed it during project generation, add it and restart the app.
Why is the /actuator/health endpoint returning a 404 in my Spring Boot app?
Secret mission
Add Alert Thresholds
Your monitor tracks whether endpoints are up or down. But what about endpoints that respond successfully yet take too long? In this secret mission, you'll add configurable latency thresholds that flag slow endpoints as "degraded" and display real-time visual warnings on the dashboard.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally with an in-memory database, so there are no ongoing costs.
Resources you used:
- The endpoint-monitor project folder on your Desktop.
- The Spring Boot application process running on port 8080.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with the health monitor.
- Your Spring Boot app will keep running until you close the terminal or shut down your machine.
- The H2 in-memory database resets every time the app restarts, so no data accumulates on disk.
- The project folder stays on your Desktop for future reference.
Pause - I'll come back to this later
Stop the running application to free up memory and port 8080. Your project files stay on disk so you can restart any time.
- Switch to the terminal where the Spring Boot app is running.
- Press Ctrl+C to stop the application.
- When you want to resume, open the endpoint-monitor folder in VS Code and run mvnw.cmd spring-boot:run again.
Delete - I don't want to use this again
Remove all project files and stop the running process. This frees up disk space and port 8080.
- Switch to the terminal where the Spring Boot app is running and press Ctrl+C to stop it.
- Delete the project folder by running the following command for your operating system.
macOS
cd ~/Desktop
rm -rf endpoint-monitor
Windows
cd ~/Desktop
rmdir /s /q endpoint-monitor
- Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see endpoint-monitor listed.
Nice Work!
Nice Work!
Nice work! You just built a production-style real-time endpoint health monitoring service from scratch using Spring Boot.
You've learned how to:
- Built a REST API with full CRUD endpoints for managing monitored targets, using Spring Data JPA and H2 to persist data.
- Implemented scheduled background tasks that periodically ping configured URLs, measure response time, and record results automatically.
- Used WebSocket with the STOMP protocol to push live health check results to a browser dashboard without polling.
- Added configurable alert thresholds that flag endpoints as degraded when response time exceeds a limit, with real-time visual warnings on the dashboard.
Ready to quiz yourself?