Event-Driven Microservices with Kafka
Build a distributed order system with Spring Boot, Kafka, and Docker Compose.
Introduction
30 Second Summary
When you place an order online, dozens of behind-the-scenes systems kick into action. The payment processor, inventory tracker, and shipping notifier all coordinate without any single system waiting for the others.
In this project, you will build an event-driven microservices system with two Spring Boot services that communicate asynchronously through Apache Kafka, all orchestrated by Docker Compose.
What You'll Build
A distributed order processing system where an Order Service publishes events to Kafka and a Notification Service consumes them, all routed through a Spring Cloud Gateway and registered with Eureka service discovery.
By the end of this project, you'll have:
- A multi-service architecture with Eureka service discovery and an API Gateway routing traffic to your microservices.
- An event-driven pipeline where Order Service publishes Kafka events consumed by Notification Service.
- A complete Docker Compose setup that brings the entire stack up with a single command, plus JUnit and Mockito tests proving the services work.
- Secret Mission: Add a Circuit Breaker with Resilience4J so when Kafka goes down, orders gracefully fall back to a retry queue.
Are there any prerequisites?
This project assumes you have Java development experience and a basic understanding of REST APIs. Familiarity with Spring Boot is helpful but not strictly required.
Before We Start
Building a microservices system means running multiple services and infrastructure components at the same time. Docker lets you spin up everything your system needs in isolated containers, so each piece runs reliably without cluttering your machine.
In this step, you will install Docker Desktop, scaffold four Spring Boot projects, and start a Kafka container that your microservices will use to communicate.
In this step, get ready to:
- Install and verify Docker Desktop.
- Scaffold four Spring Boot microservice projects via Spring Initializr.
- Start Apache Kafka in a Docker container.
Verify Docker Desktop is installed and running
Docker Desktop bundles the Docker Engine, Docker CLI, and Docker Compose into one application. You need all three to run Kafka and later containerize your microservices.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check whether Docker is installed by running this command:
docker --version
- Also check Docker Compose by running:
docker compose version
✔️ I see version numbers for both commands
Docker Desktop is installed and ready. Make sure the Docker Desktop application is running (you should see the whale icon in your system tray or menu bar).
ⓧ Command not found
Docker Desktop is not installed yet. Follow these steps to install it.
- Open your browser and go to the Docker Desktop download page.
- Click the download button for your operating system (macOS Apple Silicon, macOS Intel, or Windows).
- On macOS, double-click the downloaded Docker.dmg file, then drag the Docker icon to your Applications folder.
- On Windows, double-click Docker Desktop Installer.exe and follow the prompts. The installer will ask which installation mode you prefer.
- Once installed, open Docker Desktop from your Applications folder (macOS) or Start menu (Windows).
- Wait for the Docker Desktop dashboard to finish loading (the whale icon in your system tray should stop animating).
- Re-run docker --version and docker compose version in your terminal to confirm the installation.
Still seeing command not found after installing?
Make sure Docker Desktop is actually running. The CLI commands only work when Docker Desktop is active. On macOS, check for the whale icon in your menu bar. On Windows, check the system tray.
help me troubleshoot Docker Desktop installation
Scaffold four Spring Boot projects with Spring Initializr
Spring Initializr is a web tool that generates preconfigured Spring Boot projects with the dependencies you select. You will create four separate services that together form your microservices architecture.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project parent folder and move into it by running these commands:
mkdir event-driven-microservices
cd event-driven-microservices
Now you will generate each Spring Boot project from the Spring Initializr website.
- Open your browser and go to start.spring.io.
- Set the following common options for all four projects:
- Project: Maven
- Language: Java
- Spring Boot: select the latest 3.4.x version available.
- Group: com.example
- Packaging: Jar
- Java: 17
The only things that change per project are the Artifact name and the Dependencies. Generate each project one at a time using the settings below.
Project 1: Eureka Server
- Set Artifact to eureka-server.
- In the Dependencies search box, type and add: Eureka Server.
- Click Generate to download the zip file.
Project 2: API Gateway
- Set Artifact to api-gateway.
- In the Dependencies search box, type and add: Gateway and Eureka Discovery Client.
- Click Generate to download the zip file.
Project 3: Order Service
- Set Artifact to order-service.
- In the Dependencies search box, type and add: Spring Web, Eureka Discovery Client, and Spring for Apache Kafka.
- Click Generate to download the zip file.
Project 4: Notification Service
- Set Artifact to notification-service.
- In the Dependencies search box, type and add: Spring Web, Eureka Discovery Client, and Spring for Apache Kafka.
- Click Generate to download the zip file.
- Extract each downloaded zip file into your event-driven-microservices folder on the Desktop.
- After extracting all four, confirm the folder structure by running:
ls
You should see four folders: eureka-server, api-gateway, order-service, and notification-service.
What is Spring Initializr?
Spring Initializr is a project generator maintained by the Spring team. It creates a ready-to-run Maven or Gradle project with your chosen dependencies already wired into the build file.
Each generated project includes a pom.xml with the correct starter dependencies, a main application class, and the Maven wrapper so you can build without installing Maven globally.
Don't see all four folders?
Make sure you extracted each zip directly into the event-driven-microservices folder, not into a nested subfolder. Some unzip tools create an extra directory layer. You should see eureka-server/pom.xml directly, not eureka-server/eureka-server/pom.xml.
help me fix my Spring Initializr project folder structure
Create the Docker Compose file for Kafka
Apache Kafka is the message broker that will carry events between your Order Service and Notification Service. You will run Kafka in a Docker container using KRaft mode, which eliminates the need for a separate Zookeeper process.
- Make sure your terminal is inside the event-driven-microservices folder.
- Create a new file called docker-compose.yml by running:
touch docker-compose.yml
- Open docker-compose.yml in your IDE and paste the following configuration:
version: '3.8'
services:
kafka:
image: apache/kafka:latest
container_name: kafka
ports:
- '9092:9092'
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
What does this configuration do?
This file defines a single Kafka broker running in KRaft mode (no Zookeeper). Here is what each key setting controls:
- KAFKA_PROCESS_ROLES: broker,controller makes this single node act as both the message broker and the metadata controller.
- KAFKA_LISTENERS opens port 9092 for your applications and port 9093 for internal controller communication.
- KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 tells clients running on your host machine how to connect.
- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 is required for a single-node setup since the default of 3 would fail without multiple brokers.
- Save docker-compose.yml.
- Start the Kafka container by running:
docker compose up -d
You should see Docker pull the apache/kafka:latest image and start the container. The -d flag runs it in the background.
- Verify Kafka is running by checking the container status:
docker compose ps
You should see a kafka container with a status of Up.
Kafka container not starting?
Make sure Docker Desktop is running before you execute docker compose up -d. If you see a port conflict on 9092, another process may already be using that port. Stop it or change the host port in your compose file.
help me debug my Kafka Docker container
✔️ Awesome, I've got everything!
Great. Double check you have saved docker-compose.yml and that docker compose ps shows your Kafka container running.
ⓧ I'd like to double check the full code
Here is the complete docker-compose.yml you should have at this point:
version: '3.8'
services:
kafka:
image: apache/kafka:latest
container_name: kafka
ports:
- '9092:9092'
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
Your development environment is ready. You have Docker running Kafka in the background and four Spring Boot projects waiting to be wired together. Next up, you will configure Eureka for service discovery so your microservices can find each other automatically.
Set Up Service Discovery with Eureka
Your Kafka broker is running and all four project skeletons are in place. Now those services need a way to find each other at runtime.
In a microservices system, services start and stop on different ports and even different machines. Hardcoding addresses breaks the moment anything moves. Service discovery solves this by giving services a central registry to announce themselves and look up other services by name.
In this step, get ready to:
- Configure the Eureka Server for standalone service discovery.
- Register Order Service and Notification Service as Eureka clients.
- Verify all services appear in the Eureka dashboard.
Configure the Eureka Server application class
Eureka is a service registry from Netflix, integrated into Spring Cloud. Your eureka-server project already has the spring-cloud-starter-netflix-eureka-server dependency from scaffolding. You just need to activate it with an annotation.
- In your IDE, open eureka-server/src/main/java/com/nextwork/eurekaserver/EurekaServerApplication.java
- Replace the file contents with the following code:
package com.nextwork.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
// Activates the Eureka service registry in this application
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
What does this code do?
The @EnableEurekaServer annotation tells Spring Boot to start an embedded Eureka registry inside this application.
Other microservices will register themselves here and query this registry to discover each other by name instead of by hardcoded addresses.
- Save EurekaServerApplication.java.
Configure the Eureka Server properties
Spring Initializr generates an application.properties file by default, but YAML is more readable for nested configuration. You will replace it with an application.yml file.
- Delete the existing eureka-server/src/main/resources/application.properties file.
- Create a new file called application.yml in the same eureka-server/src/main/resources/ folder.
- Paste the following configuration into application.yml:
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
# Don't register this server with itself
register-with-eureka: false
# Don't fetch the registry (we ARE the registry)
fetch-registry: false
server:
# Skip the default wait time on startup for faster dev feedback
wait-time-in-ms-when-sync-empty: 0
What does this configuration do?
- server.port: 8761 sets the standard Eureka port that clients will connect to.
- register-with-eureka: false and fetch-registry: false tell this server not to register with itself, since it IS the registry.
- wait-time-in-ms-when-sync-empty: 0 removes the default 5-minute wait before Eureka starts serving registry data. This speeds up local development.
- Save application.yml.
Seeing a YAML parse error on startup?
YAML is indentation-sensitive. Make sure you use spaces (not tabs) and that each nested key is indented exactly 2 spaces deeper than its parent.
help me fix a YAML indentation error in my Eureka application.yml
Add the Eureka client dependency to Order Service and Notification Service
The Order Service and Notification Service need to register themselves with Eureka so the API Gateway can discover them later. This requires the spring-cloud-starter-netflix-eureka-client dependency and the Spring Cloud BOM for version management.
- Open order-service/pom.xml.
- Add the Spring Cloud BOM inside the <dependencyManagement> section (create it if it does not exist, after the </dependencies> closing tag):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2024.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- Add the Eureka client starter inside the <dependencies> section of the same file:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Why no version on the client dependency?
The Spring Cloud BOM you added in <dependencyManagement> manages versions for all Spring Cloud starters. You only declare the artifact name and the BOM resolves the compatible version automatically.
- Save order-service/pom.xml.
- Repeat the same two additions (Spring Cloud BOM and eureka-client dependency) in notification-service/pom.xml.
- Save notification-service/pom.xml.
Now configure each service to register with Eureka by creating their application.yml files.
- Delete order-service/src/main/resources/application.properties and create application.yml in the same folder.
- Paste the following configuration into the Order Service application.yml:
server:
port: 8081
spring:
application:
name: order-service
eureka:
client:
service-url:
# Register with the Eureka server running on port 8761
defaultZone: http://localhost:8761/eureka/
- Save the file.
- Delete notification-service/src/main/resources/application.properties and create application.yml in the same folder.
- Paste the following configuration into the Notification Service application.yml:
server:
port: 8082
spring:
application:
name: notification-service
eureka:
client:
service-url:
# Register with the Eureka server running on port 8761
defaultZone: http://localhost:8761/eureka/
What does defaultZone do?
The defaultZone property tells each service where to find the Eureka registry. On startup, each service sends its name, host, and port to this URL. Eureka then makes that information available to any service that queries the registry.
- Save notification-service/application.yml.
Start all services and verify the Eureka dashboard
With all configuration in place, you can start the Eureka Server first (so it is ready to accept registrations) and then start the two client services.
- Open a terminal in the eureka-server directory and start the server by running:
./mvnw spring-boot:run
- Wait until you see a log line containing Started EurekaServerApplication.
- Open a second terminal in the order-service directory and start it by running:
./mvnw spring-boot:run
- Open a third terminal in the notification-service directory and start it by running:
./mvnw spring-boot:run
- Open your browser and navigate to http://localhost:8761.
- Under the Instances currently registered with Eureka section, you should see both ORDER-SERVICE and NOTIFICATION-SERVICE listed.
What should I see?
The Eureka dashboard shows a web page with system status and a table of registered instances. Each service appears with its application name in uppercase, its status (UP), and its host and port.
Services may take up to 30 seconds to appear after starting, because Eureka clients send their first heartbeat on a 30-second interval by default.
Services not appearing in the dashboard?
- Make sure the Eureka Server started first and is running on port 8761.
- Check that defaultZone in each client's application.yml matches http://localhost:8761/eureka/ exactly (including the trailing slash).
- If you see a connection refused error in the client logs, the Eureka Server is not running yet. Wait for it to fully start before launching clients.
help me debug why my services are not registering with Eureka
✔️ Awesome, I've got everything!
Great. Make sure all three applications stay running for the next step.
ⓧ I'd like to double check the full code
Here is the full code for each file you created or modified in this step.
package com.nextwork.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
register-with-eureka: false
fetch-registry: false
server:
wait-time-in-ms-when-sync-empty: 0
server:
port: 8081
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
server:
port: 8082
spring:
application:
name: notification-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
Your services can now discover each other by name instead of hardcoded addresses. Next up, you will configure the API Gateway to route external traffic to these services using that same discovery mechanism.
Configure the API Gateway
Your services are registered with Eureka and can find each other. But right now, external clients would need to know the exact port of each service to talk to it.
An API Gateway solves this by providing a single entry point for all traffic. Clients hit one URL, and the gateway routes requests to the correct microservice behind the scenes using service discovery.
In this step, get ready to:
- Verify the api-gateway project has the required dependencies.
- Define routes that forward requests to your Order Service.
- Test the gateway by sending a request through it.
Verify the gateway dependencies
The api-gateway project was scaffolded in Step 1 using Spring Initializr. It should already include the two dependencies needed to function as a gateway with service discovery.
- In your IDE, open the pom.xml file inside the api-gateway project folder.
- Confirm that the <dependencies> section includes spring-cloud-starter-gateway (the gateway engine itself).
- Confirm it also includes spring-cloud-starter-netflix-eureka-client (so the gateway registers with Eureka and discovers other services).
Why does the gateway need Eureka?
Without Eureka, the gateway would need hardcoded URLs for every service it routes to. By registering as a Eureka client, the gateway can look up services by name and automatically route to healthy instances.
Missing one of the dependencies?
If either dependency is not present, you likely missed selecting it during Spring Initializr scaffolding. Add the missing <dependency> entry to your pom.xml manually. The group ID for both is org.springframework.cloud.
help me fix missing gateway dependencies in my pom.xml
Define the gateway routes
Spring Cloud Gateway uses YAML configuration to define routes. Each route has an ID, a destination URI, and predicates that determine which requests match.
- Inside the api-gateway project, navigate to src/main/resources.
- If an application.properties file exists there, delete it.
- Create a new file called application.yml in the same src/main/resources folder.
- Paste the following configuration into application.yml:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
What does this configuration do?
- server.port: 8080 sets the gateway to listen on port 8080. This becomes the single entry point for all external traffic.
- spring.application.name: api-gateway gives this service a name that Eureka uses to register it.
- The routes section defines one route with ID order-service.
- uri: lb://order-service tells the gateway to look up the service named order-service in Eureka and load-balance requests to it. The lb:// prefix means "use service discovery for load balancing".
- Path=/api/orders/** is a predicate that matches any request whose path starts with /api/orders/.
- Now add the Eureka client configuration. Paste the following at the bottom of the same file:
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
What does the Eureka section do?
This tells the gateway where to find the Eureka server. The gateway will register itself there and query it to resolve service names (like order-service) to actual host:port addresses.
- Save application.yml.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file, then continue below.
ⓧ I'd like to double check the full code
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
Test the gateway routing
With the configuration in place, you can start the gateway and verify it routes traffic through to the Order Service.
- Make sure your eureka-server and order-service are still running from earlier.
- Run the api-gateway application. In your IDE, open ApiGatewayApplication.java and run it, or use Maven from the api-gateway directory by running:
mvn spring-boot:run
- Wait for the application to start. You should see a log line indicating the gateway is listening on port 8080.
- Open your browser and navigate to http://localhost:8761. Confirm that API-GATEWAY now appears in the list of registered instances alongside ORDER-SERVICE and NOTIFICATION-SERVICE.
- Test the route by sending a request through the gateway. Open a new terminal and run:
curl http://localhost:8080/api/orders
What should I see?
You should receive a 404 JSON error response from the Order Service. This is expected because the Order Service does not have a /api/orders REST controller yet (you will build that in the next step).
The important thing is that you got a response at all. A 404 from order-service proves the gateway successfully discovered the service via Eureka and forwarded your request to it. If routing had failed, you would see a 503 Service Unavailable from the gateway itself.
Seeing a 503 or connection refused?
- Check that order-service is still running on port 8081.
- Verify ORDER-SERVICE appears in the Eureka dashboard at http://localhost:8761. If it does not appear, the gateway cannot discover it.
- Confirm your application.yml uses lb://order-service (lowercase) because the URI must match the spring.application.name of the target service.
help me debug why my Spring Cloud Gateway is returning 503 instead of routing to my service
Your API Gateway is live and routing traffic. Next up, you will build the Order Service REST API and Kafka producer so that /api/orders actually does something useful.
Build the Order Service REST API and Kafka Producer
Your API Gateway is routing traffic to the Order Service, but right now the service has no endpoints to handle requests. In this step, you'll build the REST API that accepts customer orders and a Kafka producer that broadcasts order events to any downstream service that needs them.
This is the core of event-driven architecture in action. Instead of the Order Service calling the Notification Service directly, it publishes an event to a Kafka topic. Any service that cares about new orders can subscribe independently.
In this step, get ready to:
- Create the Order model, in-memory repository, and REST controller.
- Add Kafka support and build the event publisher.
- Wire the publisher into the controller and test the full flow with curl.
Create the Order model and REST API
Every microservice needs a domain model. The Order Service will store orders in memory and expose them through a REST endpoint.
- In the order-service project, create a new package com.nextwork.orderservice.model and add a file called Order.java with the following code:
package com.nextwork.orderservice.model;
import java.util.UUID;
public class Order {
private String id;
private String product;
private int quantity;
// Default constructor generates a unique ID
public Order() {
this.id = UUID.randomUUID().toString();
}
// Parameterized constructor for creating orders with product details
public Order(String product, int quantity) {
this.id = UUID.randomUUID().toString();
this.product = product;
this.quantity = quantity;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getProduct() { return product; }
public void setProduct(String product) { this.product = product; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}
What does this code do?
The Order class is a simple POJO (Plain Old Java Object) that represents a customer order.
- Each order gets a unique ID via UUID.randomUUID() so you can track it through the system.
- The no-arg constructor is required for JSON deserialization when Spring receives a request body.
- Getters and setters allow Spring's Jackson library to convert between JSON and Java objects.
- You won't see a change yet. The next file uses this model to store orders.
- Create a new package com.nextwork.orderservice.repository and add a file called OrderRepository.java with the following code:
package com.nextwork.orderservice.repository;
import com.nextwork.orderservice.model.Order;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
// Marks this class as a Spring-managed data access component
@Repository
public class OrderRepository {
// Thread-safe list to store orders in memory
private final List<Order> orders = new CopyOnWriteArrayList<>();
public Order save(Order order) {
orders.add(order);
return order;
}
public List<Order> findAll() {
return new ArrayList<>(orders);
}
}
What does this code do?
This is a simple in-memory repository that stores orders in a list instead of a database.
- CopyOnWriteArrayList is thread-safe, which matters because multiple HTTP requests could hit the service at the same time.
- findAll() returns a defensive copy so callers cannot accidentally modify the internal list.
- The @Repository annotation tells Spring to manage this class as a bean and inject it where needed.
- You won't see a change yet. The next file exposes these operations over HTTP.
- Create a new package com.nextwork.orderservice.controller and add a file called OrderController.java with the following code:
package com.nextwork.orderservice.controller;
import com.nextwork.orderservice.model.Order;
import com.nextwork.orderservice.repository.OrderRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
// Exposes order endpoints under /api/orders
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderRepository orderRepository;
public OrderController(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
// Accepts a JSON order body and persists it
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
Order saved = orderRepository.save(order);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
// Returns all orders stored so far
@GetMapping
public ResponseEntity<List<Order>> getOrders() {
return ResponseEntity.ok(orderRepository.findAll());
}
}
What does this code do?
This controller handles incoming HTTP requests for orders.
- @RestController combines @Controller and @ResponseBody so every method returns JSON directly.
- @RequestMapping("/api/orders") sets the base path, matching what the API Gateway routes here.
- The POST endpoint saves the order and returns a 201 Created response.
- The GET endpoint returns all orders for quick verification.
- Save all three files.
- Stop the running order-service (press Ctrl+C in its terminal) and restart it by running:
cd order-service
mvn spring-boot:run
- Test the GET endpoint by running this in a new terminal:
curl http://localhost:8081/api/orders
- You should see an empty JSON array [] in the response.
Seeing a compilation error?
- Check that your package declarations match the folder structure exactly. The package com.nextwork.orderservice.model must live in src/main/java/com/nextwork/orderservice/model/.
- Verify that the import in OrderController.java matches the package you used for Order.java and OrderRepository.java.
Help me debug my Order Service compilation error
Add Kafka support and create the event publisher
The REST API works, but right now creating an order is a dead end. You need the Order Service to broadcast an event whenever a new order arrives so other services can react.
- Open the pom.xml file in the order-service project.
- Find the <dependencies> section and add the following dependency:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
What does this dependency do?
The spring-kafka library provides Spring Boot auto-configuration for Apache Kafka. It gives you a pre-configured KafkaTemplate bean that you can inject into any class to send messages.
You omit the version number because Spring Boot's dependency management automatically selects the compatible version.
- You won't see a change yet. The next file configures where Kafka lives and how to serialize messages.
- Open order-service/src/main/resources/application.yml and replace its contents with the following:
server:
port: 8081
spring:
application:
name: order-service
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
What does this configuration do?
- bootstrap-servers: localhost:9092 tells Spring Kafka where your Kafka broker is running (the Docker container from Step 1).
- key-serializer uses StringSerializer because your message keys will be order IDs (strings).
- value-serializer uses JsonSerializer so Spring automatically converts your Order objects to JSON before sending them to Kafka.
- You won't see a change yet. The next file uses this configuration to publish events.
- Create a new package com.nextwork.orderservice.event and add a file called OrderEventPublisher.java with the following code:
package com.nextwork.orderservice.event;
import com.nextwork.orderservice.model.Order;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
// Publishes order events to a Kafka topic
@Component
public class OrderEventPublisher {
private static final String TOPIC = "order-events";
private final KafkaTemplate<String, Order> kafkaTemplate;
public OrderEventPublisher(KafkaTemplate<String, Order> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
// Sends the order to Kafka with the order ID as the message key
public void publishOrderCreated(Order order) {
kafkaTemplate.send(TOPIC, order.getId(), order);
}
}
What does this code do?
- KafkaTemplate<String, Order> is auto-configured by Spring Boot thanks to the spring-kafka dependency and your application.yml settings.
- kafkaTemplate.send(TOPIC, order.getId(), order) publishes the order to the order-events topic. Using the order ID as the key ensures all events for the same order land in the same Kafka partition, preserving ordering.
- @Component registers this class as a Spring bean so it can be injected into the controller.
Why publish events instead of calling services directly?
In a synchronous design, the Order Service would call the Notification Service via HTTP. If that service is down, order creation fails too.
With event-driven architecture, the Order Service just publishes to Kafka and moves on. Kafka stores the event durably. The Notification Service (and any future subscribers) can consume it whenever they are ready.
- Save OrderEventPublisher.java.
Wire the publisher into the controller and test
The event publisher exists, but the controller does not use it yet. You need to inject the publisher and call it after every order is saved.
- Open OrderController.java and replace its entire contents with the updated version below:
package com.nextwork.orderservice.controller;
import com.nextwork.orderservice.event.OrderEventPublisher;
import com.nextwork.orderservice.model.Order;
import com.nextwork.orderservice.repository.OrderRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderRepository orderRepository;
private final OrderEventPublisher orderEventPublisher;
// Constructor now injects both the repository and the event publisher
public OrderController(OrderRepository orderRepository, OrderEventPublisher orderEventPublisher) {
this.orderRepository = orderRepository;
this.orderEventPublisher = orderEventPublisher;
}
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
Order saved = orderRepository.save(order);
// Publish an event to Kafka after saving
orderEventPublisher.publishOrderCreated(saved);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@GetMapping
public ResponseEntity<List<Order>> getOrders() {
return ResponseEntity.ok(orderRepository.findAll());
}
}
What changed?
- The controller now declares OrderEventPublisher as a dependency alongside the repository.
- Spring injects both via constructor injection automatically.
- After saving an order, orderEventPublisher.publishOrderCreated(saved) fires the event to Kafka.
- Save OrderController.java.
- Stop the running order-service and restart it by running:
mvn spring-boot:run
- Make sure your Kafka container is still running. Verify by running:
docker compose up -d
- Test the full flow by sending a POST request through the API Gateway:
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"product": "Laptop", "quantity": 2}'
- You should see a JSON response with a generated id, the product Laptop, and quantity 2.
- Check the order-service terminal logs. You should see a log line confirming the message was sent to the order-events topic.
Getting a connection refused error?
- Make sure your Kafka Docker container is running. Run docker ps and confirm you see the Kafka container listening on port 9092.
- Verify the bootstrap-servers value in your application.yml is exactly localhost:9092.
- If you restarted Docker Desktop, run docker compose up -d again from the project root to bring Kafka back.
Help me debug my Kafka connection error in Spring Boot
✔️ Awesome, I've got everything!
Great. Double check you have saved all files in the order-service project.
ⓧ I'd like to double check the full code
Here is the full code for each file you created or modified in this step.
package com.nextwork.orderservice.model;
import java.util.UUID;
public class Order {
private String id;
private String product;
private int quantity;
public Order() {
this.id = UUID.randomUUID().toString();
}
public Order(String product, int quantity) {
this.id = UUID.randomUUID().toString();
this.product = product;
this.quantity = quantity;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getProduct() { return product; }
public void setProduct(String product) { this.product = product; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}
package com.nextwork.orderservice.repository;
import com.nextwork.orderservice.model.Order;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Repository
public class OrderRepository {
private final List<Order> orders = new CopyOnWriteArrayList<>();
public Order save(Order order) {
orders.add(order);
return order;
}
public List<Order> findAll() {
return new ArrayList<>(orders);
}
}
package com.nextwork.orderservice.controller;
import com.nextwork.orderservice.event.OrderEventPublisher;
import com.nextwork.orderservice.model.Order;
import com.nextwork.orderservice.repository.OrderRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderRepository orderRepository;
private final OrderEventPublisher orderEventPublisher;
public OrderController(OrderRepository orderRepository, OrderEventPublisher orderEventPublisher) {
this.orderRepository = orderRepository;
this.orderEventPublisher = orderEventPublisher;
}
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
Order saved = orderRepository.save(order);
orderEventPublisher.publishOrderCreated(saved);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@GetMapping
public ResponseEntity<List<Order>> getOrders() {
return ResponseEntity.ok(orderRepository.findAll());
}
}
package com.nextwork.orderservice.event;
import com.nextwork.orderservice.model.Order;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
@Component
public class OrderEventPublisher {
private static final String TOPIC = "order-events";
private final KafkaTemplate<String, Order> kafkaTemplate;
public OrderEventPublisher(KafkaTemplate<String, Order> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishOrderCreated(Order order) {
kafkaTemplate.send(TOPIC, order.getId(), order);
}
}
server:
port: 8081
spring:
application:
name: order-service
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
Your Order Service is now a fully functional Kafka producer. Every time a customer places an order, an event flies into the order-events topic. Next up, you'll build the Notification Service consumer that listens for those events and acts on them.
Build the Notification Service Kafka Consumer
Your Order Service is publishing events into Kafka every time a new order arrives. But right now, nobody is listening.
In this step, you'll build the consumer side of the event-driven pipeline. The Notification Service will subscribe to the order-events topic and log a confirmation every time it receives an order event.
In this step, get ready to:
- Configure the Notification Service as a Kafka consumer.
- Create a listener that processes order events and logs notifications.
- Test the full end-to-end flow from the API Gateway through to the Notification Service.
Add the spring-kafka dependency and configure the consumer
Just like the Order Service needed Spring Kafka to produce messages, the Notification Service needs it to consume messages. Spring Boot auto-configures the consumer once the library is on the classpath and the right properties are set.
- Open notification-service/pom.xml in your IDE.
- Find the <dependencies> section and add the following dependency:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
Why no version number?
Spring Boot's dependency management automatically resolves the correct version of spring-kafka that's compatible with your Spring Boot version. You only need the groupId and artifactId.
Now you need to tell Spring Kafka where to find the broker and how to deserialize incoming messages.
- Open notification-service/src/main/resources/application.yml.
- Replace the entire contents with the following configuration:
server:
port: 8082
spring:
application:
name: notification-service
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: notification-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
What does this configuration do?
bootstrap-servers: localhost:9092 points the consumer at your running Kafka broker.
group-id: notification-group assigns this consumer to a consumer group. Kafka uses consumer groups to track which messages have been read.
auto-offset-reset: earliest tells the consumer to start reading from the beginning of the topic if no previous offset exists. The spring.json.trusted.packages: "*" property allows the JSON deserializer to accept objects from any package.
- Save application.yml.
IDE showing errors on the kafka properties?
If your IDE underlines the spring.kafka section, make sure you saved the pom.xml changes first. Your IDE needs to reload the Maven dependencies before it recognizes the new configuration keys.
In IntelliJ, click the Maven reload icon (circular arrows) in the top right. In VS Code, the Java extension reloads automatically after saving.
Help me fix Kafka configuration errors in my Spring Boot application.
Create the NotificationListener class
With the consumer configured, you need a class that actually processes incoming messages. Spring Kafka's @KafkaListener annotation marks a method as a message handler. Spring automatically calls it whenever a new message arrives on the specified topic.
- In your IDE, create a new package called listener inside notification-service/src/main/java/com/nextwork/notificationservice/.
- Create a new file called NotificationListener.java inside the listener package.
- Add the following code to NotificationListener.java:
package com.nextwork.notificationservice.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class NotificationListener {
// Logger for outputting notification messages
private static final Logger log = LoggerFactory.getLogger(NotificationListener.class);
// Listens to the order-events topic as part of the notification-group consumer group
@KafkaListener(topics = "order-events", groupId = "notification-group")
public void handleOrderCreated(Map<String, Object> order) {
// Log the notification with the order ID
log.info("Notification sent for order: {}", order.get("id"));
}
}
What does this code do?
@KafkaListener(topics = "order-events", groupId = "notification-group") tells Spring Kafka to subscribe this method to the order-events topic. Whenever the Order Service publishes a new event, this method receives it.
The method accepts a Map<String, Object> instead of a typed Order object. This keeps the Notification Service decoupled from the Order Service. It doesn't need to know the exact Order class structure.
Why use a Map instead of an Order class?
In a real microservices system, each service owns its own domain models. If the Notification Service imported the Order class directly, it would create tight coupling between the two services. Using a generic Map keeps them independent.
In production, teams often use a shared schema registry or contract-first API design to handle this. For now, a Map is the simplest decoupled approach.
- Save NotificationListener.java.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Here's the complete application.yml for notification-service:
server:
port: 8082
spring:
application:
name: notification-service
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: notification-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
And here's the complete NotificationListener.java:
package com.nextwork.notificationservice.listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class NotificationListener {
private static final Logger log = LoggerFactory.getLogger(NotificationListener.class);
@KafkaListener(topics = "order-events", groupId = "notification-group")
public void handleOrderCreated(Map<String, Object> order) {
log.info("Notification sent for order: {}", order.get("id"));
}
}
Test the full end-to-end event flow
Now for the exciting part. You'll send an order through the API Gateway, watch it flow into the Order Service, get published to Kafka, and arrive at the Notification Service.
- Stop the notification-service if it's currently running (press Ctrl+C in its terminal).
- Restart the notification-service by running this from the notification-service directory:
mvn spring-boot:run
- Wait until you see a log line indicating the consumer has started. Look for a message that mentions notification-group and topic assignment.
What should I see?
In the notification-service logs, you should see Spring Kafka report that it has assigned partitions for the order-events topic to the notification-group consumer. This confirms your listener is ready to receive messages.
- Make sure your eureka-server, api-gateway, and order-service are all still running. Also confirm the Kafka container is still up.
- Open a new terminal window and send a test order through the API Gateway by running:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d '{"product": "Laptop", "quantity": 1}'
- Check the terminal where notification-service is running.
You should see a log line like:
Notification sent for order: <some-uuid>
That log confirms the full event-driven pipeline is working. The order went in through the gateway, was persisted by the Order Service, published to Kafka as an event, and consumed by the Notification Service.
Not seeing the notification log?
Make sure the Kafka container is still running. Run docker ps and confirm you see the Kafka container listed. If it's not there, run docker compose up -d from your project root to restart it.
Check that the notification-service logs show the consumer group partition assignment (mentioned above). If you don't see it, the service may not have connected to Kafka. Verify your bootstrap-servers value in application.yml is exactly localhost:9092.
Also verify the topic name in your @KafkaListener annotation matches the topic in OrderEventPublisher. Both should be order-events.
Help me debug why my Kafka consumer isn't receiving messages from the order-events topic.
Your event-driven pipeline is fully operational. Next up, you'll write unit tests to verify this logic works without needing the full infrastructure running.
Write Unit Tests with JUnit 5 and Mockito
Your microservices system is running end-to-end, with orders flowing through the API Gateway to Kafka and arriving at the Notification Service. But how do you prove each piece works in isolation without spinning up the whole stack?
Unit tests let you verify individual components by mocking their dependencies. In this step, you'll write fast, isolated tests for the Order Service using JUnit 5 and Mockito.
In this step, get ready to:
- Write a controller unit test using @WebMvcTest with mocked dependencies.
- Write a publisher unit test to verify Kafka interactions.
- Run the full test suite and verify green results.
Write a unit test for OrderController
The OrderController depends on both the OrderRepository and the OrderEventPublisher. To test the controller in isolation, you'll mock both dependencies so the test never touches Kafka or the real repository.
- In your IDE, create a new file at order-service/src/test/java/com/nextwork/orderservice/controller/OrderControllerTest.java.
- You may need to create the controller folder inside src/test/java/com/nextwork/orderservice/ first.
- Paste the following test class into the file:
package com.nextwork.orderservice.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nextwork.orderservice.event.OrderEventPublisher;
import com.nextwork.orderservice.model.Order;
import com.nextwork.orderservice.repository.OrderRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
// Loads only the web layer for this specific controller
@WebMvcTest(OrderController.class)
public class OrderControllerTest {
// MockMvc simulates HTTP requests without starting a real server
@Autowired
private MockMvc mockMvc;
// Replace the real OrderRepository with a Mockito mock
@MockBean
private OrderRepository orderRepository;
// Replace the real OrderEventPublisher with a Mockito mock
@MockBean
private OrderEventPublisher orderEventPublisher;
// Jackson ObjectMapper to serialize Order objects to JSON
@Autowired
private ObjectMapper objectMapper;
@Test
void createOrder_shouldReturnCreatedAndPublishEvent() throws Exception {
// Arrange: create a test order and configure mock behavior
Order order = new Order("Widget", 5);
when(orderRepository.save(any(Order.class))).thenReturn(order);
// Act: perform a POST request with the order as JSON
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(order)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.product").value("Widget"));
// Assert: verify the event publisher was called exactly once
verify(orderEventPublisher, times(1)).publishOrderCreated(any(Order.class));
}
}
What does this test do?
@WebMvcTest(OrderController.class) boots only the web layer for the specified controller. It does not start Kafka, Eureka, or any other infrastructure.
@MockBean replaces real beans in the Spring context with Mockito mocks. The test verifies that when a POST request arrives, the controller saves the order and publishes an event, without actually touching Kafka.
Why mock instead of using a real Kafka broker?
Unit tests should be fast and isolated. Mocking KafkaTemplate tests your business logic without starting Kafka infrastructure. Integration tests with an embedded broker test the full flow but are slower. Both are valuable at different levels of the test pyramid.
- Save OrderControllerTest.java.
Seeing import errors in your IDE?
Make sure your pom.xml includes the spring-boot-starter-test dependency. Spring Initializr adds it by default. If it is missing, add it to your <dependencies> block with <scope>test</scope>.
help me fix import errors in my Spring Boot test file
✔️ Awesome, I've got everything!
Great. Double check you have saved the file before moving on.
ⓧ I'd like to double check the full code
package com.nextwork.orderservice.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nextwork.orderservice.event.OrderEventPublisher;
import com.nextwork.orderservice.model.Order;
import com.nextwork.orderservice.repository.OrderRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(OrderController.class)
public class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderRepository orderRepository;
@MockBean
private OrderEventPublisher orderEventPublisher;
@Autowired
private ObjectMapper objectMapper;
@Test
void createOrder_shouldReturnCreatedAndPublishEvent() throws Exception {
Order order = new Order("Widget", 5);
when(orderRepository.save(any(Order.class))).thenReturn(order);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(order)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.product").value("Widget"));
verify(orderEventPublisher, times(1)).publishOrderCreated(any(Order.class));
}
}
Write a unit test for OrderEventPublisher
The OrderEventPublisher wraps KafkaTemplate and sends events to the order-events topic. You need to verify it calls send() with the correct topic name and payload.
- Create a new file at order-service/src/test/java/com/nextwork/orderservice/event/OrderEventPublisherTest.java.
- Create the event folder inside src/test/java/com/nextwork/orderservice/ if it does not exist.
- Paste the following test class into the file:
package com.nextwork.orderservice.event;
import com.nextwork.orderservice.model.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.kafka.core.KafkaTemplate;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
// Enables Mockito annotations without starting the Spring context
@ExtendWith(MockitoExtension.class)
public class OrderEventPublisherTest {
// Create a mock KafkaTemplate instead of a real one
@Mock
private KafkaTemplate<String, Order> kafkaTemplate;
// Inject the mock into the publisher under test
@InjectMocks
private OrderEventPublisher orderEventPublisher;
@Test
void publishOrderCreated_shouldSendToCorrectTopic() {
// Arrange: create a test order
Order order = new Order("Gadget", 2);
// Act: publish the order event
orderEventPublisher.publishOrderCreated(order);
// Assert: verify KafkaTemplate.send() was called with the right arguments
verify(kafkaTemplate, times(1)).send("order-events", order.getId(), order);
}
}
What does this test do?
@ExtendWith(MockitoExtension.class) activates Mockito annotations without loading any Spring context. This makes the test extremely fast.
@Mock creates a fake KafkaTemplate, and @InjectMocks automatically injects it into the OrderEventPublisher constructor. The verify() call confirms that send() was invoked with topic order-events, the order ID as the key, and the order object as the value.
- Save OrderEventPublisherTest.java.
MockitoExtension not found?
The spring-boot-starter-test dependency bundles both JUnit 5 and Mockito. If your IDE cannot resolve MockitoExtension, try refreshing your Maven project. In IntelliJ, right-click the pom.xml and select Maven then Reload Project.
help me resolve MockitoExtension import issues in my Spring Boot test
✔️ Awesome, I've got everything!
Great. Double check you have saved the file before moving on.
ⓧ I'd like to double check the full code
package com.nextwork.orderservice.event;
import com.nextwork.orderservice.model.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.kafka.core.KafkaTemplate;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
public class OrderEventPublisherTest {
@Mock
private KafkaTemplate<String, Order> kafkaTemplate;
@InjectMocks
private OrderEventPublisher orderEventPublisher;
@Test
void publishOrderCreated_shouldSendToCorrectTopic() {
Order order = new Order("Gadget", 2);
orderEventPublisher.publishOrderCreated(order);
verify(kafkaTemplate, times(1)).send("order-events", order.getId(), order);
}
}
Run the test suite with Maven
Both tests are written. Time to run them and confirm everything passes.
- Open a terminal in the order-service directory.
- Run the full test suite by running this command:
mvn test
- Wait for Maven to compile and run the tests. You should see output ending with something like:
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Both tests passing confirms that your controller correctly delegates to the repository and publisher, and that the publisher sends events to the right Kafka topic with the correct payload.
Seeing test failures?
If you see compilation errors, check that your test file package declarations match the directory structure exactly. The package must be com.nextwork.orderservice.controller for the controller test and com.nextwork.orderservice.event for the publisher test.
If you see a NoSuchBeanDefinitionException, make sure both @MockBean fields are present in OrderControllerTest. Missing either one means Spring cannot wire the controller.
help me debug my failing Spring Boot unit tests
Your Order Service now has a proper test safety net. Next up, you'll containerize the entire system with Docker so it all runs from a single command.
Dockerize the Full Stack
Your services work perfectly when running individually on your machine. In a real production environment, every service needs to be packaged into a container so the entire system can be deployed consistently across any host.
In this step, you'll containerize all four Spring Boot services using multi-stage Docker builds and orchestrate the full stack with a single Docker Compose command.
In this step, get ready to:
- Add multi-stage Dockerfiles to each Spring Boot service.
- Update Docker Compose to orchestrate the full system with health checks.
- Launch and verify the entire stack with one command.
Add a Dockerfile to each Spring Boot service
A multi-stage Docker build uses one image to compile the application and a separate, smaller image to run it. The build stage includes the full JDK and Maven. The runtime stage only includes the JRE and the compiled JAR file.
- Create a file called Dockerfile in the order-service folder (at the same level as pom.xml and src).
- Paste the following into order-service/Dockerfile:
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
What does this Dockerfile do?
The first stage (build) uses a Maven image with JDK 17 to compile the project and produce a JAR file. The -DskipTests flag avoids running tests during the Docker build since you already tested locally.
The second stage uses a JRE-only image, copies only the compiled JAR from the build stage, and runs it. This keeps the final image small because it excludes Maven, the JDK, and all source code.
- Create a Dockerfile in the notification-service folder with the same content, but change EXPOSE 8081 to EXPOSE 8082:
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8082
ENTRYPOINT ["java", "-jar", "app.jar"]
- Create a Dockerfile in the eureka-server folder with EXPOSE 8761:
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8761
ENTRYPOINT ["java", "-jar", "app.jar"]
- Create a Dockerfile in the api-gateway folder with EXPOSE 8080:
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
- Save all four Dockerfiles.
- Confirm each file exists by checking your project structure. You should see a Dockerfile at the root of each service folder.
Why multi-stage builds?
A single-stage build that includes Maven and the JDK produces images around 600 MB or more. Multi-stage builds produce images around 200-300 MB because the final image only has the JRE and your compiled JAR.
This matters in production where smaller images mean faster deployments, less storage, and a reduced attack surface.
Dockerfile not being picked up?
Make sure the file is named exactly Dockerfile with a capital D and no file extension. It should sit alongside pom.xml and src/ in each service folder.
Help me troubleshoot my Dockerfile placement for my Spring Boot multi-service project.
Update Docker Compose and application configs for container networking
When services run inside Docker containers, they can no longer use localhost to reach each other. Instead, they use the container service names defined in docker-compose.yml as hostnames.
- Open order-service/src/main/resources/application.yml and find this line:
bootstrap-servers: localhost:9092
- Replace localhost:9092 with kafka:9092. Also find the Eureka line:
defaultZone: http://localhost:8761/eureka/
- Replace localhost:8761 with eureka-server:8761. The full file should now look like this:
server:
port: 8081
spring:
application:
name: order-service
kafka:
bootstrap-servers: kafka:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
- Open notification-service/src/main/resources/application.yml and make the same two changes. Replace localhost:9092 with kafka:9092 and localhost:8761 with eureka-server:8761:
server:
port: 8082
spring:
application:
name: notification-service
kafka:
bootstrap-servers: kafka:9092
consumer:
group-id: notification-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
- Open api-gateway/src/main/resources/application.yml and replace localhost:8761 with eureka-server:8761:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
- Save all three application.yml files.
Why replace localhost with service names?
Inside a Docker Compose network, each service gets a hostname matching its service name in the YAML file. When the order-service container tries to reach Kafka, it resolves kafka to the Kafka container's IP address automatically.
Using localhost inside a container would point to the container itself, not to the host machine or other containers.
Now update the docker-compose.yml to include all four Spring Boot services alongside Kafka.
- Open docker-compose.yml in your project root and replace its entire contents with the following:
version: '3.8'
services:
kafka:
image: apache/kafka:latest
container_name: kafka
ports:
- '9092:9092'
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
healthcheck:
test: ["CMD", "kafka-topics.sh", "--bootstrap-server", "localhost:9092", "--list"]
interval: 10s
timeout: 5s
retries: 5
eureka-server:
build: ./eureka-server
container_name: eureka-server
ports:
- '8761:8761'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8761/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
api-gateway:
build: ./api-gateway
container_name: api-gateway
ports:
- '8080:8080'
depends_on:
eureka-server:
condition: service_healthy
order-service:
build: ./order-service
container_name: order-service
ports:
- '8081:8081'
depends_on:
kafka:
condition: service_healthy
eureka-server:
condition: service_healthy
notification-service:
build: ./notification-service
container_name: notification-service
ports:
- '8082:8082'
depends_on:
kafka:
condition: service_healthy
eureka-server:
condition: service_healthy
What does this Compose file do?
The healthcheck blocks define commands Docker runs to verify a service is actually ready. Kafka's health check lists topics to confirm the broker is accepting connections. Eureka's health check hits the Spring Boot Actuator endpoint.
The depends_on with condition: service_healthy means Docker Compose waits until the dependency passes its health check before starting the dependent service. This prevents your services from crashing because Kafka or Eureka is not ready yet.
The build: property points to each service's directory, where Docker Compose finds the Dockerfile and builds the image.
- Save docker-compose.yml.
Eureka health check failing?
The Eureka health check uses curl to hit /actuator/health. This requires spring-boot-starter-actuator in your eureka-server/pom.xml. If you did not include it when scaffolding the project, add it as a dependency now.
Help me add Spring Boot Actuator to my eureka-server project for Docker health checks.
Build and run the full stack
With Dockerfiles in place and the Compose file updated, you can now bring the entire system up with a single command.
- First, stop any services still running locally (Eureka, Gateway, Order Service, Notification Service). Close their terminal windows or press Ctrl+C in each.
- Stop the existing Kafka container by running:
docker compose down
- Build and start the entire stack by running:
docker compose up --build
What does this command do?
The --build flag tells Docker Compose to build fresh images from each Dockerfile before starting the containers. Without it, Compose would use cached images that might not reflect your latest code changes.
The first build downloads Maven dependencies for each service, so it may take several minutes. Subsequent builds are faster because Docker caches the dependency layers.
- Wait for all services to start. You should see log output from all five containers (kafka, eureka-server, api-gateway, order-service, notification-service).
- Once you see the services registering with Eureka in the logs, open a new terminal and test the end-to-end flow by running:
curl -X POST http://localhost:8080/api/orders -H "Content-Type: application/json" -d '{"product": "Dockerized Widget", "quantity": 3}'
- You should see a JSON response with the order details and a generated ID.
- Switch back to the terminal running Docker Compose. Look for a log line from the notification-service that says Notification sent for order: followed by the order ID.
What should I see?
The curl command hits the API Gateway container on port 8080. The gateway routes the request to the Order Service container. Order Service saves the order and publishes a Kafka event. The Notification Service container consumes the event and logs the notification. The entire flow works identically to when you ran everything locally, but now it is fully containerized.
Services failing to start?
If you see connection refused errors in the logs, the health checks may not have enough time. Increase the retries value in the health check configuration.
If a service cannot find Kafka at kafka:9092, confirm you changed bootstrap-servers from localhost:9092 to kafka:9092 in the service's application.yml.
Help me debug why my Docker Compose services cannot connect to each other.
✔️ Awesome, I've got everything!
Great. Double check you have saved all files and your curl test returned a successful response.
ⓧ I'd like to double check the full code
Here is the complete docker-compose.yml:
version: '3.8'
services:
kafka:
image: apache/kafka:latest
container_name: kafka
ports:
- '9092:9092'
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
healthcheck:
test: ["CMD", "kafka-topics.sh", "--bootstrap-server", "localhost:9092", "--list"]
interval: 10s
timeout: 5s
retries: 5
eureka-server:
build: ./eureka-server
container_name: eureka-server
ports:
- '8761:8761'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8761/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
api-gateway:
build: ./api-gateway
container_name: api-gateway
ports:
- '8080:8080'
depends_on:
eureka-server:
condition: service_healthy
order-service:
build: ./order-service
container_name: order-service
ports:
- '8081:8081'
depends_on:
kafka:
condition: service_healthy
eureka-server:
condition: service_healthy
notification-service:
build: ./notification-service
container_name: notification-service
ports:
- '8082:8082'
depends_on:
kafka:
condition: service_healthy
eureka-server:
condition: service_healthy
Here is the order-service/Dockerfile (the others are identical except for the EXPOSE port):
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
Here is the updated order-service/src/main/resources/application.yml:
server:
port: 8081
spring:
application:
name: order-service
kafka:
bootstrap-servers: kafka:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
Here is the updated notification-service/src/main/resources/application.yml:
server:
port: 8082
spring:
application:
name: notification-service
kafka:
bootstrap-servers: kafka:9092
consumer:
group-id: notification-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
Here is the updated api-gateway/src/main/resources/application.yml:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
Your entire microservices system now runs as a single Docker Compose stack. Next up, you'll tackle the Secret Mission and add fault tolerance with a Circuit Breaker pattern.
Secret mission
Add a Circuit Breaker with Resilience4J
What happens when Kafka goes down? Right now, orders fail silently. Add a Circuit Breaker to the Order Service so it gracefully degrades, storing failed events in a retry queue instead of crashing.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Docker, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers (kafka, eureka-server, api-gateway, order-service, notification-service).
- Docker images built for each Spring Boot service.
- Project folder on your Desktop containing all source code and configuration.
- Docker Desktop application.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with the microservices system.
- Your Docker containers will continue running until you manually stop them or restart your machine.
- Docker Desktop will remain installed and available for future projects.
Pause - I'll come back to this later
Shut down running containers to free up memory and CPU, but keep your project files and Docker images for later.
- Open your terminal and navigate to your project folder (the folder containing your docker-compose.yml).
- Stop all containers by running:
docker compose down
This stops and removes the containers but keeps your images and project files intact. Next time you want to resume, run docker compose up --build from the same folder.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes containers, images, volumes, and your source code.
- Open your terminal and navigate to your project folder (the folder containing your docker-compose.yml).
- Stop all containers and remove the associated images and volumes by running:
docker compose down --rmi all --volumes
- Navigate up to your Desktop and delete the project folder.
macOS
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]
Windows
cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
- To uninstall Docker Desktop on macOS, open Docker Desktop, click the Troubleshoot icon (bug icon), and select Uninstall.
- To uninstall Docker Desktop on Windows, go to Settings, then Apps, then Apps & features. Select Docker Desktop and click Uninstall.
Nice Work!
Nice Work!
You just built a production-style distributed system from scratch. Your microservices architecture uses the exact patterns that come up in architect-level interviews: service discovery, API gateway routing, event-driven communication, containerization, and testing.
Here's what you accomplished:
- Built two Spring Boot microservices that communicate asynchronously via Apache Kafka events, with an Order Service publishing and a Notification Service consuming.
- Set up Eureka for service discovery and routed all traffic through Spring Cloud Gateway with load-balanced service resolution.
- Dockerized the entire stack with Docker Compose, bringing up Kafka in KRaft mode, Eureka, Gateway, and both services with a single command, plus wrote JUnit 5 and Mockito tests to prove the services work.
- Secret Mission: Added fault tolerance with a Resilience4J Circuit Breaker so the Order Service degrades gracefully when Kafka is unavailable.
Ready to quiz yourself?