Skip to content

Mindblownserver/Spring-Microservice-Repo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

4 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ—οΈ TP: Building Microservices with Spring Boot


πŸ“‹ Table of Contents

  1. Introduction & Objectives
  2. Architecture Overview
  3. Step 1 β€” Create the product-service
  4. Step 2 β€” Create the cart-service
  5. Step 3 β€” Inter-Service Communication with ActiveMQ
  6. Step 4 β€” API Gateway with Spring Cloud Gateway
  7. Step 5 β€” Service Discovery with Eureka
  8. Step 6 β€” Final Testing & Wrap Up

1 β€” Introduction & Objectives

What is a Microservice?

A microservice is a small, independent, deployable application that focuses on a single business capability. Unlike a monolithic application where all features live in one codebase, microservices split the system into small autonomous services. alt text

What will we build?

A simple e-commerce application with two microservices:

Service Responsibility
product-service Manage products (add, list)
cart-service Manage the shopping cart (add, list, delete items)

Scenarios to implement

# Scenario Service
1 Add one or many products to the database product-service
2 Get the product list product-service
3 Add a product to the cart cart-service
4 Get cart products cart-service
5 Delete a product from the cart cart-service
6 Delete all products from the cart cart-service

Infrastructure components

Component Role
ActiveMQ Message broker β€” allows product-service to notify cart-service when a product is created or updated
API Gateway Single entry point β€” routes client requests to the correct microservice
Eureka Server Service discovery β€” services register themselves, and the gateway looks them up dynamically

Why this order?

We intentionally keep Eureka for the last step. This way, you will first experience the pain of hardcoding service URLs, and then understand why dynamic service discovery is needed.


2 β€” Architecture Overview

alt text

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚   Client     β”‚
                        β”‚ (Postman)    β”‚
                        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                        β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”
                        β”‚  API Gateway β”‚
                        β”‚  (port 8080) β”‚
                        β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
                           β”‚       β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”   β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚product-serviceβ”‚   β”‚ cart-service  β”‚
              β”‚  (port 8081)  β”‚   β”‚  (port 8082)  β”‚
              β”‚   [H2 DB]    β”‚   β”‚   [H2 DB]     β”‚
              β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚                   β”‚
                     β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
                     └──►│ ActiveMQ  β”‚β”€β”€β”€β”˜
                         β”‚(port 61616β”‚
                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Each microservice has its own H2 in-memory database β€” this is a key principle: database per service.


Step 1 β€” Create the product-service

1.1 Generate the project

  1. Open IntelliJ IDEA
  2. Go to File β†’ New β†’ Project
  3. Select Spring Initializr (on the left panel)
  4. Fill in:
    • Group: com.ecommerce
    • Artifact: product-service
    • Name: product-service
    • Packaging: Jar
    • Java version: 17
  5. Click Next and add the following dependencies:
    • βœ… Spring Web
    • βœ… Spring Data JPA
    • βœ… H2 Database
    • βœ… Spring Boot DevTools
  6. Click Create

πŸ’‘ IntelliJ Tip: If you don't see Spring Initializr, make sure you have the "Spring" plugin enabled in Settings β†’ Plugins.

1.2 Project structure

After creation, your project should look like this:

product-service/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main/
β”‚   β”‚   β”œβ”€β”€ java/com/ecommerce/productservice/
β”‚   β”‚   β”‚   └── ProductServiceApplication.java
β”‚   β”‚   └── resources/
β”‚   β”‚       └── application.properties
β”‚   └── test/
β”œβ”€β”€ pom.xml

1.3 Configure H2 Database

Open src/main/resources/application.properties and add:

# Server port
server.port=8081

# Application name
spring.application.name=product-service

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:productdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# Enable H2 Console (accessible at http://localhost:8081/h2-console)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

πŸ’‘ What is H2? H2 is an in-memory database. It's perfect for development and testing because it doesn't require installation. The data disappears when the application stops.

πŸ§ͺ Try it: After starting the app, visit http://localhost:8081/h2-console. Use JDBC URL jdbc:h2:mem:productdb to connect.

1.4 Create the Product Entity

Create a new package model inside com.ecommerce.productservice, then create the class:

package com.ecommerce.productservice.model;

import jakarta.persistence.*;

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Double price;

    private String description;

    // Default constructor (required by JPA)
    public Product() {}

    // Constructor with fields
    public Product(String name, Double price, String description) {
        this.name = name;
        this.price = price;
        this.description = description;
    }

    // Getters and Setters
    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 Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
}

πŸ“ Explanation:

  • @Entity tells JPA this class maps to a database table
  • @Id + @GeneratedValue make id an auto-incremented primary key
  • @Column(nullable = false) ensures the field cannot be null in the database

1.5 Create the Repository

Create a new package repository:

package com.ecommerce.productservice.repository;

import com.ecommerce.productservice.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    // JpaRepository provides: save(), findAll(), findById(), deleteById(), etc.
    // No need to write any SQL!
}

πŸ“ Explanation: By extending JpaRepository<Product, Long>, Spring Data JPA automatically generates the implementation for common CRUD operations. Product is the entity type, Long is the type of the primary key.

1.6 Create the Service Layer

Create a new package service:

package com.ecommerce.productservice.service;

import com.ecommerce.productservice.model.Product;
import com.ecommerce.productservice.repository.ProductRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    // Constructor injection (recommended over @Autowired on fields)
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    /**
     * Save a single product
     */
    public Product addProduct(Product product) {
        return productRepository.save(product);
    }

    /**
     * Save multiple products at once
     */
    public List<Product> addProducts(List<Product> products) {
        return productRepository.saveAll(products);
    }

    /**
     * Get all products
     */
    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }

    /**
     * Find a product by ID
     */
    public Product getProductById(Long id) {
        return productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
    }
}

1.7 Create the REST Controller

Create a new package controller:

package com.ecommerce.productservice.controller;

import com.ecommerce.productservice.model.Product;
import com.ecommerce.productservice.service.ProductService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    /**
     * POST /api/products
     * Add a single product
     * Body: { "name": "Laptop", "price": 999.99, "description": "A powerful laptop" }
     */
    @PostMapping
    public ResponseEntity<Product> addProduct(@RequestBody Product product) {
        Product savedProduct = productService.addProduct(product);
        return new ResponseEntity<>(savedProduct, HttpStatus.CREATED);
    }

    /**
     * POST /api/products/batch
     * Add multiple products at once
     * Body: [ { "name": "Laptop", ... }, { "name": "Phone", ... } ]
     */
    @PostMapping("/batch")
    public ResponseEntity<List<Product>> addProducts(@RequestBody List<Product> products) {
        List<Product> savedProducts = productService.addProducts(products);
        return new ResponseEntity<>(savedProducts, HttpStatus.CREATED);
    }

    /**
     * GET /api/products
     * Get all products
     */
    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        List<Product> products = productService.getAllProducts();
        return ResponseEntity.ok(products);
    }

    /**
     * GET /api/products/{id}
     * Get a single product by ID
     */
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        Product product = productService.getProductById(id);
        return ResponseEntity.ok(product);
    }
}

1.8 Test the product-service

  1. Run the application: Right-click ProductServiceApplication.java β†’ Run
  2. Open Postman (or use curl)

Test 1: Add a single product

POST http://localhost:8081/api/products
Content-Type: application/json

{
    "name": "Laptop HP",
    "price": 1299.99,
    "description": "HP Pavilion 15 - 16GB RAM"
}

Expected response (201 Created):

{
    "id": 1,
    "name": "Laptop HP",
    "price": 1299.99,
    "description": "HP Pavilion 15 - 16GB RAM"
}

Test 2: Add multiple products

POST http://localhost:8081/api/products/batch
Content-Type: application/json

[
    { "name": "iPhone 15", "price": 999.99, "description": "Apple iPhone 15 Pro" },
    { "name": "Samsung Galaxy S24", "price": 899.99, "description": "Samsung flagship" },
    { "name": "AirPods Pro", "price": 249.99, "description": "Wireless earbuds" }
]

Test 3: Get all products

GET http://localhost:8081/api/products

βœ… Checkpoint: You should see all 4 products returned. If yes, move to Step 2!


Step 2 β€” Create the cart-service

2.1 Generate the project

  1. Open IntelliJ IDEA β†’ File β†’ New β†’ Project (or New Module if using a multi-module setup)
  2. Select Spring Initializr
  3. Fill in:
    • Group: com.ecommerce
    • Artifact: cart-service
    • Name: cart-service
  4. Add dependencies:
    • βœ… Spring Web
    • βœ… Spring Data JPA
    • βœ… H2 Database
    • βœ… Spring Boot DevTools
  5. Click Create

2.2 Configure H2 Database

Open src/main/resources/application.properties:

# Server port (different from product-service!)
server.port=8082

# Application name
spring.application.name=cart-service

# H2 Database Configuration β€” SEPARATE database!
spring.datasource.url=jdbc:h2:mem:cartdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

πŸ”‘ Key point: Notice that spring.datasource.url is jdbc:h2:mem:cartdb β€” a different database than productdb. Each microservice owns its data!

2.3 Create the CartItem Entity

package com.ecommerce.cartservice.model;

import jakarta.persistence.*;

@Entity
@Table(name = "cart_items")
public class CartItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private Long productId;

    @Column(nullable = false)
    private String productName;

    @Column(nullable = false)
    private Double productPrice;

    @Column(nullable = false)
    private Integer quantity;

    public CartItem() {}

    public CartItem(Long productId, String productName, Double productPrice, Integer quantity) {
        this.productId = productId;
        this.productName = productName;
        this.productPrice = productPrice;
        this.quantity = quantity;
    }

    // Getters and Setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public Long getProductId() { return productId; }
    public void setProductId(Long productId) { this.productId = productId; }

    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }

    public Double getProductPrice() { return productPrice; }
    public void setProductPrice(Double productPrice) { this.productPrice = productPrice; }

    public Integer getQuantity() { return quantity; }
    public void setQuantity(Integer quantity) { this.quantity = quantity; }
}

πŸ“ Why do we store productName and productPrice here? Because in microservices, each service is autonomous. The cart-service cannot query the product-service's database directly. We denormalize the data to avoid tight coupling.

2.4 Create the Repository

package com.ecommerce.cartservice.repository;

import com.ecommerce.cartservice.model.CartItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface CartItemRepository extends JpaRepository<CartItem, Long> {

    Optional<CartItem> findByProductId(Long productId);

    void deleteByProductId(Long productId);
}

2.5 Create the Service Layer

package com.ecommerce.cartservice.service;

import com.ecommerce.cartservice.model.CartItem;
import com.ecommerce.cartservice.repository.CartItemRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class CartService {

    private final CartItemRepository cartItemRepository;

    public CartService(CartItemRepository cartItemRepository) {
        this.cartItemRepository = cartItemRepository;
    }

    /**
     * Add a product to the cart.
     * If the product already exists, increment the quantity.
     */
    public CartItem addToCart(CartItem cartItem) {
        Optional<CartItem> existingItem = cartItemRepository.findByProductId(cartItem.getProductId());

        if (existingItem.isPresent()) {
            CartItem item = existingItem.get();
            item.setQuantity(item.getQuantity() + cartItem.getQuantity());
            return cartItemRepository.save(item);
        }

        return cartItemRepository.save(cartItem);
    }

    /**
     * Get all items in the cart
     */
    public List<CartItem> getCartItems() {
        return cartItemRepository.findAll();
    }

    /**
     * Remove a specific product from the cart
     */
    @Transactional
    public void removeFromCart(Long productId) {
        cartItemRepository.deleteByProductId(productId);
    }

    /**
     * Clear the entire cart
     */
    public void clearCart() {
        cartItemRepository.deleteAll();
    }
}

2.6 Create the DTO (Data Transfer Object)

Create a package dto:

package com.ecommerce.cartservice.dto;

public class AddToCartRequest {

    private Long productId;
    private String productName;
    private Double productPrice;
    private Integer quantity;

    public AddToCartRequest() {}

    // Getters and Setters
    public Long getProductId() { return productId; }
    public void setProductId(Long productId) { this.productId = productId; }

    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }

    public Double getProductPrice() { return productPrice; }
    public void setProductPrice(Double productPrice) { this.productPrice = productPrice; }

    public Integer getQuantity() { return quantity; }
    public void setQuantity(Integer quantity) { this.quantity = quantity; }
}

2.7 Create the REST Controller

package com.ecommerce.cartservice.controller;

import com.ecommerce.cartservice.dto.AddToCartRequest;
import com.ecommerce.cartservice.model.CartItem;
import com.ecommerce.cartservice.service.CartService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/cart")
public class CartController {

    private final CartService cartService;

    public CartController(CartService cartService) {
        this.cartService = cartService;
    }

    /**
     * POST /api/cart
     * Add a product to the cart
     */
    @PostMapping
    public ResponseEntity<CartItem> addToCart(@RequestBody AddToCartRequest request) {
        CartItem cartItem = new CartItem(
                request.getProductId(),
                request.getProductName(),
                request.getProductPrice(),
                request.getQuantity()
        );
        CartItem savedItem = cartService.addToCart(cartItem);
        return new ResponseEntity<>(savedItem, HttpStatus.CREATED);
    }

    /**
     * GET /api/cart
     * Get all items in the cart
     */
    @GetMapping
    public ResponseEntity<List<CartItem>> getCartItems() {
        List<CartItem> items = cartService.getCartItems();
        return ResponseEntity.ok(items);
    }

    /**
     * DELETE /api/cart/{productId}
     * Remove a specific product from the cart
     */
    @DeleteMapping("/{productId}")
    public ResponseEntity<String> removeFromCart(@PathVariable Long productId) {
        cartService.removeFromCart(productId);
        return ResponseEntity.ok("Product " + productId + " removed from cart");
    }

    /**
     * DELETE /api/cart
     * Clear the entire cart
     */
    @DeleteMapping
    public ResponseEntity<String> clearCart() {
        cartService.clearCart();
        return ResponseEntity.ok("Cart cleared successfully");
    }
}

2.8 Test the cart-service

  1. Run the CartServiceApplication
  2. Make sure product-service is still running on port 8081

Test 1: Add a product to the cart

POST http://localhost:8082/api/cart
Content-Type: application/json

{
    "productId": 1,
    "productName": "Laptop HP",
    "productPrice": 1299.99,
    "quantity": 1
}

Test 2: Add another product

POST http://localhost:8082/api/cart
Content-Type: application/json

{
    "productId": 2,
    "productName": "iPhone 15",
    "productPrice": 999.99,
    "quantity": 2
}

Test 3: Get cart contents

GET http://localhost:8082/api/cart

Test 4: Delete a specific product from cart

DELETE http://localhost:8082/api/cart/1

Test 5: Delete all products from cart

DELETE http://localhost:8082/api/cart

βœ… Checkpoint: All 5 cart operations work? Great! But notice something: the two services don't communicate at all yet. When a product is added to product-service, cart-service doesn't know about it. Let's fix that!


Step 3 β€” Inter-Service Communication with ActiveMQ

3.1 What is ActiveMQ and why do we need it?

Right now, our two services are completely isolated. In a real e-commerce app, when a product is created or its price changes, the cart-service should be notified.

Two approaches for inter-service communication:

Approach Type Example
REST call (HTTP) Synchronous cart-service calls product-service API directly
Message broker Asynchronous product-service publishes a message, cart-service consumes it

We use ActiveMQ (a message broker) because:

  • βœ… Services are decoupled β€” they don't need to know each other's URLs
  • βœ… Fault tolerant β€” if cart-service is down, messages wait in the queue
  • βœ… Scalable β€” multiple consumers can read from the same queue

3.2 Install and Start ActiveMQ

  1. Download ActiveMQ Classic from: https://activemq.apache.org/components/classic/download/
  2. Extract the archive
  3. Start ActiveMQ:
    • Windows: Run bin\win64\activemq.bat start
    • macOS/Linux: Run ./bin/activemq start
  4. Verify: Open http://localhost:8161 in your browser
    • Login: admin / admin
    • You should see the ActiveMQ web console

πŸ’‘ Alternative (Docker): If you have Docker:

docker run -d --name activemq -p 61616:61616 -p 8161:8161 apache/activemq-classic:latest

3.3 Add ActiveMQ Dependency to Both Services

Add the following dependency to the pom.xml of both product-service and cart-service:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

πŸ“ After adding this, click the Maven reload icon πŸ”„ in IntelliJ (or right-click pom.xml β†’ Maven β†’ Reload Project).

3.4 Configure ActiveMQ in Both Services

Add to application.properties of both services:

# ActiveMQ Configuration
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin

3.5 Product Service β€” The Producer (Sender)

When a product is created, product-service will send a message to a queue called product-queue.

Create the message producer in product-service:

package com.ecommerce.productservice.messaging;

import com.ecommerce.productservice.model.Product;
//import com.fasterxml.jackson.core.JsonProcessingException;
//import com.fasterxml.jackson.databind.ObjectMapper;
//import org.springframework.boot.json.JsonParseException;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.exc.JsonNodeException;

@Component
public class ProductMessageProducer {

    private final JmsTemplate jmsTemplate;
    private final ObjectMapper objectMapper;

    public ProductMessageProducer(JmsTemplate jmsTemplate, ObjectMapper objectMapper) {
        this.jmsTemplate = jmsTemplate;
        this.objectMapper = objectMapper;
    }

    /**
     * Send product information to the "product-queue"
     */
    public void sendProductMessage(Product product) {
        try {
            String productJson = objectMapper.writeValueAsString(product);
            jmsTemplate.convertAndSend("product-queue", productJson);
            System.out.println("πŸ“€ Message sent to product-queue: " + productJson);
        } catch (JsonNodeException e) {
            System.err.println("❌ Error serializing product: " + e.getMessage());
        }
    }
}

πŸ“ Explanation:

  • JmsTemplate is Spring's helper to send JMS messages
  • We convert the Product object to JSON (String) before sending
  • "product-queue" is the name of our message queue

Update the ProductService to send a message after saving:

package com.ecommerce.productservice.service;

import com.ecommerce.productservice.messaging.ProductMessageProducer;
import com.ecommerce.productservice.model.Product;
import com.ecommerce.productservice.repository.ProductRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

    private final ProductRepository productRepository;
    private final ProductMessageProducer messageProducer;

    public ProductService(ProductRepository productRepository, ProductMessageProducer messageProducer) {
        this.productRepository = productRepository;
        this.messageProducer = messageProducer;
    }

    public Product addProduct(Product product) {
        Product savedProduct = productRepository.save(product);
        // πŸ”” Notify other services via ActiveMQ
        messageProducer.sendProductMessage(savedProduct);
        return savedProduct;
    }

    public List<Product> addProducts(List<Product> products) {
        List<Product> savedProducts = productRepository.saveAll(products);
        // πŸ”” Notify for each product
        savedProducts.forEach(messageProducer::sendProductMessage);
        return savedProducts;
    }

    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }

    public Product getProductById(Long id) {
        return productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
    }
}

3.6 Cart Service β€” The Consumer (Listener)

cart-service will listen to the product-queue and log received product information.

Create a DTO for the product message in cart-service:

package com.ecommerce.cartservice.dto;

public class ProductMessage {

    private Long id;
    private String name;
    private Double price;
    private String description;

    public ProductMessage() {}

    // Getters and Setters
    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 Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    @Override
    public String toString() {
        return "ProductMessage{id=" + id + ", name='" + name + "', price=" + price + "}";
    }
}

Create the message consumer:

package com.ecommerce.cartservice.messaging;

import com.ecommerce.cartservice.dto.ProductMessage;
//import com.fasterxml.jackson.core.JsonProcessingException;
//import com.fasterxml.jackson.databind.ObjectMapper;
//import org.springframework.boot.json.JsonParseException;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.exc.JsonNodeException;

@Component
public class ProductMessageConsumer {

    private final ObjectMapper objectMapper;

    public ProductMessageConsumer(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * Listen to "product-queue" and process incoming product messages
     */
    @JmsListener(destination = "product-queue")
    public void receiveProductMessage(String message) {
        try {
            ProductMessage product = objectMapper.readValue(message, ProductMessage.class);
            System.out.println("πŸ“₯ Received product message: " + product);
            System.out.println("   β†’ Product '" + product.getName() + "' is now available (price: " + product.getPrice() + "€)");
            // In a real app, you could cache this product info locally
            // or update cart prices if a product price changed
        } catch (JsonNodeException e) {
            System.err.println("❌ Error deserializing product message: " + e.getMessage());
        }
    }
}

πŸ“ Explanation:

  • @JmsListener(destination = "product-queue") β€” this method is automatically called whenever a new message arrives in the queue
  • Spring handles threading, connection management, and acknowledgment

3.7 Test the ActiveMQ Integration

  1. Make sure ActiveMQ is running
  2. Restart both product-service and cart-service
  3. Add a new product:
POST http://localhost:8081/api/products
Content-Type: application/json

{
    "name": "MacBook Pro",
    "price": 2499.99,
    "description": "Apple MacBook Pro M3"
}
  1. Check the console of cart-service β€” you should see:
πŸ“₯ Received product message: ProductMessage{id=1, name='MacBook Pro', price=2499.99}
   β†’ Product 'MacBook Pro' is now available (price: 2499.99€)
  1. Check ActiveMQ console at http://localhost:8161 β†’ Queues β†’ You should see product-queue with messages enqueued/dequeued.

βœ… Checkpoint: The two services now communicate asynchronously! The product-service doesn't need to know where cart-service is β€” it just sends a message to the broker.


Step 4 β€” API Gateway with Spring Cloud Gateway

4.1 Why do we need an API Gateway?

Right now, the client needs to know:

  • Product service is at localhost:8081
  • Cart service is at localhost:8082

Problems:

  • ❌ The client needs to know every service's address and port
  • ❌ If we add more services, the client code must change
  • ❌ No centralized security, logging, or rate limiting

Solution: An API Gateway acts as a single entry point. The client only talks to the gateway! alt text

4.2 Create the API Gateway Project

  1. File β†’ New β†’ Project β†’ Spring Initializr
  2. Fill in:
    • Group: com.ecommerce
    • Artifact: api-gateway
    • Name: api-gateway
  3. Dependencies:
    • βœ… Gateway (Spring Cloud Routing β†’ Gateway)

⚠️ Make sure to select Spring Cloud Gateway (reactive), not Spring MVC.

4.3 Configure the Gateway

Open src/main/resources/application.properties (or use application.yml β€” we'll use .yml here for clarity):

Create/rename to application.yml:

server:
  port: 8080

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      server:
        webmvc:
          routes:
              # Route to product-service
              - id: product-service
                uri: http://localhost:8081
                predicates:
                  - Path=/api/products/**
      
              # Route to cart-service
              - id: cart-service
                uri: http://localhost:8082
                predicates:
                  - Path=/api/cart/**

πŸ“ Explanation:

  • All requests to http://localhost:8080/api/products/** are forwarded to http://localhost:8081/api/products/**
  • All requests to http://localhost:8080/api/cart/** are forwarded to http://localhost:8082/api/cart/**
  • The client only knows about port 8080!

4.4 Test through the Gateway

  1. Start the api-gateway application
  2. Now use port 8080 for ALL requests:
GET http://localhost:8080/api/products
POST http://localhost:8080/api/cart
GET http://localhost:8080/api/cart
DELETE http://localhost:8080/api/cart/1

βœ… Checkpoint: Everything works through port 8080! But notice β€” we hardcoded localhost:8081 and localhost:8082 in the gateway config. What if a service changes port? What if we run multiple instances? That's what Eureka solves!


Step 5 β€” Service Discovery with Eureka

5.1 The Problem We're Solving

Ask yourself these questions:

  1. What happens if product-service moves to port 9091 instead of 8081?
    • β†’ You must update the gateway config and restart it ❌
  2. What if you run 3 instances of product-service for load balancing?
    • β†’ The gateway doesn't know about the other instances ❌
  3. What if a service crashes?
    • β†’ The gateway still tries to route to it ❌

Eureka solves all of this! Each service registers itself with Eureka. The gateway asks Eureka where services are. If a service dies, Eureka removes it.

Before Eureka:                    After Eureka:
                                  
Client β†’ Gateway β†’ hardcoded URL  Client β†’ Gateway β†’ asks Eureka β†’ routes dynamically

alt text

5.2 Create the Eureka Server

  1. File β†’ New β†’ Project β†’ Spring Initializr
  2. Fill in:
    • Group: com.ecommerce
    • Artifact: eureka-server
    • Name: eureka-server
  3. Dependencies:
    • βœ… Eureka Server (Spring Cloud Discovery β†’ Eureka Server)

5.3 Configure the Eureka Server

Main Application Class β€” Add @EnableEurekaServer:

package com.ecommerce.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);
    }
}

application.properties:

server.port=8761

spring.application.name=eureka-server

# Don't register itself as a client
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

Start the Eureka Server and visit: http://localhost:8761

You should see the Eureka Dashboard β€” currently with no registered services.

5.4 Register product-service with Eureka

Add dependency to product-service/pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

You also need to add Spring Cloud BOM (Bill of Materials) in the <dependencyManagement> section. Add this to pom.xml if it's not already there:

<properties>
    <spring-cloud.version>2024.0.0</spring-cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

⚠️ Match the Spring Cloud version to your Spring Boot version. Check https://spring.io/projects/spring-cloud for compatibility.

Add to application.properties:

# Eureka Client Configuration
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
eureka.instance.prefer-ip-address=true

5.5 Register cart-service with Eureka

Repeat the exact same steps as 5.4 for cart-service:

  1. Add spring-cloud-starter-netflix-eureka-client dependency
  2. Add Spring Cloud BOM
  3. Add Eureka client config to application.properties

5.6 Update the API Gateway

Add Eureka client to api-gateway/pom.xml (same dependency as above).

Update application.yml to use service names instead of hardcoded URLs:

server:
  port: 8080

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      server:
        webmvc:
          routes:
            # Route to product-service (using Eureka service name!)
            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/api/products/**

            # Route to cart-service (using Eureka service name!)
            - id: cart-service
              uri: lb://cart-service
              predicates:
                - Path=/api/cart/**

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

πŸ“ Key change: uri: http://localhost:8081 β†’ uri: lb://product-service

  • lb:// means load-balanced β€” the gateway asks Eureka for the address
  • product-service matches the spring.application.name in that service's config

5.7 Test Everything Together

Start order:

  1. 🟒 Eureka Server (port 8761) β€” start first, wait for it to be ready
  2. 🟒 product-service (port 8081)
  3. 🟒 cart-service (port 8082)
  4. 🟒 api-gateway (port 8080)

Check Eureka Dashboard: http://localhost:8761

You should see:

Application Status
PRODUCT-SERVICE UP (1)
CART-SERVICE UP (1)
API-GATEWAY UP (1)

Test through the gateway (same as before):

# Add a product (goes through gateway β†’ Eureka β†’ product-service)
POST http://localhost:8080/api/products
Content-Type: application/json

{
    "name": "Wireless Mouse",
    "price": 29.99,
    "description": "Logitech wireless mouse"
}

# Check cart-service console for ActiveMQ message!

# Get all products
GET http://localhost:8080/api/products

# Add to cart
POST http://localhost:8080/api/cart
Content-Type: application/json

{
    "productId": 1,
    "productName": "Wireless Mouse",
    "productPrice": 29.99,
    "quantity": 3
}

# Get cart
GET http://localhost:8080/api/cart

5.8 Experiment: See Eureka in Action πŸ§ͺ

Experiment 1: Kill and restart a service

  1. Stop product-service
  2. Wait 30 seconds
  3. Check Eureka Dashboard β€” PRODUCT-SERVICE should show as DOWN
  4. Try GET http://localhost:8080/api/products β€” you'll get an error (503)
  5. Restart product-service β€” it re-registers automatically!

Experiment 2: Run multiple instances

  1. In IntelliJ, go to Run β†’ Edit Configurations
  2. Duplicate ProductServiceApplication
  3. Add VM option: -Dserver.port=8083
  4. Run both instances
  5. Check Eureka β€” you'll see PRODUCT-SERVICE with UP (2)
  6. The gateway automatically load-balances between them!

Step 6 β€” Final Testing & Wrap Up

Complete Flow Test

Execute this full scenario through the gateway (http://localhost:8080):

1. POST /api/products/batch       β†’ Add 3 products
2. GET  /api/products             β†’ Verify all 3 exist
3. POST /api/cart                 β†’ Add product 1 to cart (qty: 2)
4. POST /api/cart                 β†’ Add product 2 to cart (qty: 1)
5. GET  /api/cart                 β†’ See 2 items in cart
6. DELETE /api/cart/1             β†’ Remove product 1
7. GET  /api/cart                 β†’ Only product 2 remains
8. DELETE /api/cart               β†’ Clear cart
9. GET  /api/cart                 β†’ Empty cart []

Also check:

Architecture Summary

Component Role Port
Eureka Server Service registry β€” services register here, gateway looks them up 8761
API Gateway Single entry point β€” routes requests to correct service 8080
Product Service Manages products + sends messages to ActiveMQ 8081
Cart Service Manages cart + receives messages from ActiveMQ 8082
ActiveMQ Message broker β€” async communication between services 61616 / 8161

Key Concepts Learned

Concept What you learned
Microservices Split a monolith into independent services with separate databases
Database per Service Each service owns its data (H2 productdb vs cartdb)
Asynchronous Messaging Services communicate via ActiveMQ without direct coupling
API Gateway Single entry point for clients, hides service topology
Service Discovery Eureka eliminates hardcoded URLs, enables dynamic routing
Load Balancing lb:// prefix in gateway enables client-side load balancing

Discussion Questions πŸ’¬

  1. Why didn't we use a single shared database? β†’ Microservice independence, scalability, different data models per service.

  2. What would happen if ActiveMQ goes down? β†’ Messages would be lost (unless we configure persistence). The services still work independently.

  3. How would you add authentication? β†’ Add Spring Security to the API Gateway β€” centralized authentication.

  4. How would you extend this to include an order-service? β†’ New microservice, new database, listens to cart events, produces order events.


πŸ“ Final Project Structure

ecommerce-microservices/
β”‚
β”œβ”€β”€ eureka-server/               (port 8761)
β”‚   └── src/main/java/.../EurekaServerApplication.java
β”‚
β”œβ”€β”€ api-gateway/                 (port 8080)
β”‚   └── src/main/resources/application.yml
β”‚
β”œβ”€β”€ product-service/             (port 8081)
β”‚   └── src/main/java/.../
β”‚       β”œβ”€β”€ model/Product.java
β”‚       β”œβ”€β”€ repository/ProductRepository.java
β”‚       β”œβ”€β”€ service/ProductService.java
β”‚       β”œβ”€β”€ controller/ProductController.java
β”‚       └── messaging/ProductMessageProducer.java
β”‚
β”œβ”€β”€ cart-service/                (port 8082)
β”‚   └── src/main/java/.../
β”‚       β”œβ”€β”€ model/CartItem.java
β”‚       β”œβ”€β”€ repository/CartItemRepository.java
β”‚       β”œβ”€β”€ service/CartService.java
β”‚       β”œβ”€β”€ controller/CartController.java
β”‚       β”œβ”€β”€ dto/AddToCartRequest.java
β”‚       β”œβ”€β”€ dto/ProductMessage.java
β”‚       └── messaging/ProductMessageConsumer.java

πŸŽ‰ Congratulations! You have successfully built a microservices-based e-commerce application with:

  • Two independent services with separate databases
  • Asynchronous messaging with ActiveMQ
  • Centralized routing with API Gateway
  • Dynamic service discovery with Eureka

Bon courage ! πŸš€

About

This repo holds my lab on making microservice backend application using Spring. It uses Eureka server and Api gateway to connect two different microservices "Cart service" and "Product service"

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors