A complete, runnable two-app Spring Boot project — Producer exposes a REST API, Consumer calls it using RestTemplate. Every single file explained line-by-line in comments. Copy, run, learn.
2 Spring Boot AppsREST API (Producer)RestTemplate (Consumer)Spring Data JPAMySQL DatabaseCommandLineRunner
Consumer App→HTTP GET/POST→Producer API→Service Layer→JPA Repository→MySQL DB
01 — The Concept
What Are Microservices?
“Instead of one giant app doing everything, you build many small apps that each do ONE thing well”
🍕 Analogy: Food Court vs One Big Restaurant
A traditional app is like one huge restaurant that does pizza, burgers, sushi, and desserts all in one kitchen — if the pizza oven breaks, the entire restaurant is down. Microservices is like a food court — separate independent stalls: Pizza Stall, Burger Stall, Sushi Bar. If the pizza oven breaks, you can still get a burger. They're independent, deployed separately, and communicate with each other.
🏠 Monolith (Traditional)
One huge codebase / one deployable JAR
All layers (web, business, DB) in one project
One bug can crash the whole app
Hard to scale individual features
Example: One Spring Boot app with everything
🍒 Microservices
Multiple small, independent Spring Boot apps
Each app has its own database (optionally)
They talk to each other via REST / HTTP
Each app can be scaled and deployed independently
Example: Producer app + Consumer app
In this demo: Producer = Customer Service (owns DB, exposes REST API). Consumer = a client app (calls Producer's API using RestTemplate). This is the simplest possible microservice pattern — direct HTTP communication.
02 — Architecture
System Architecture
“Two separate JVMs. One database. One HTTP conversation.”
🖥
Consumer App
port 9090 CommandLineRunner RestTemplate
→
HTTP GET /api/customers
🌐
Producer App
port 8080 @RestController @Service
→
JPA Query
📈
MySQL DB
customerdb customers table
🌐 Producer App :8080
Owns and manages the Customer data
Has its own MySQL database
Exposes REST endpoints that return JSON
Layers: Controller → Service → DAO → DB
Anyone can call it via HTTP — decoupled
🖥 Consumer App :9090
Does NOT have its own database
Uses RestTemplate to call Producer's API
Receives JSON, deserializes to Java objects
Implements CommandLineRunner — runs on startup
Could be a mobile backend, reporting service, etc.
03 — Project Structure
Project File Structure
“Two separate Maven projects, each with their own pom.xml and main class”
🌐 producer-app port 8080
producer-app/
📄 pom.xml
src/main/java/com/demo/producer/
☕ ProducerApp.java ← main class
bean/
☕ CustomerBean.java ← @Entity
dao/
☕ CustomerDAO.java ← JpaRepository
service/
☕ CustomerService.java ← interface
☕ CustomerServiceImpl.java
controller/
☕ CustomerController.java ← @RestController
src/main/resources/
⚙ application.properties
🖥 consumer-app port 9090
consumer-app/
📄 pom.xml
src/main/java/com/demo/consumer/
☕ ConsumerApp.java ← main class
bean/
☕ CustomerBean.java ← plain POJO (mirror)
config/
☕ AppConfig.java ← @Bean RestTemplate
service/
☕ CustomerConsumerService.java
runner/
☕ AppRunner.java ← CommandLineRunner
src/main/resources/
⚙ application.properties
⚠ Important: These are TWO SEPARATE projects
Open each folder separately in your IDE as its own Maven project. Run ProducerApp.java first (starts on 8080), then run ConsumerApp.java (starts on 9090 and immediately calls the Producer).
04 — Producer App Code
🌐 Producer App port 8080
“This app OWNS the data. It has a DB, REST endpoints, and JPA. Other apps call IT.”
📄 producer-app/pom.xml
<?xml version="1.0" encoding="UTF-8"?><!--
pom.xml — Maven build config for the Producer app.
This pulls in:
- spring-boot-starter-web → REST + embedded Tomcat (port 8080)
- spring-boot-starter-data-jpa → JPA + Hibernate (ORM layer)
- mysql-connector-j → JDBC driver to connect to MySQL
-->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>producer-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<!-- Spring Boot parent handles all version management for you -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<!-- Web layer: enables @RestController, @GetMapping etc + embeds Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA layer: enables @Entity, JpaRepository, Hibernate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL JDBC driver: physical connection to MySQL database -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
# ─────────────────────────────────────────────────
# Producer App Configuration
# This app OWNS the database. It runs on port 8080.
# ─────────────────────────────────────────────────# Port where this REST API listens. Consumer will call: http://localhost:8080/...
server.port=8080
# JDBC URL — tells Hibernate where the MySQL database is.
# "customerdb" is the schema/database name — create it first in MySQL:
# CREATE DATABASE customerdb;
spring.datasource.url=jdbc:mysql://localhost:3306/customerdb
# MySQL root credentials (change to your actual username/password)
spring.datasource.username=root
spring.datasource.password=root
# MySQL driver class (required for JDBC connection)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ddl-auto=update → Hibernate auto-creates or updates the "customers" table
# on startup based on your @Entity class. For production, use "validate" instead.
spring.jpa.hibernate.ddl-auto=update
# Prints the actual SQL that Hibernate generates — very useful for learning!
spring.jpa.show-sql=true
# Formats the printed SQL so it's readable
spring.jpa.properties.hibernate.format_sql=true
# MySQL dialect tells Hibernate to generate MySQL-compatible SQL syntax
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
☕ ProducerApp.java — Main Class
package com.demo.producer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @SpringBootApplication is a shortcut for 3 annotations combined:
* 1. @Configuration — this class can define @Bean methods
* 2. @EnableAutoConfiguration — Spring Boot auto-configures everything
* (Tomcat, JPA, DataSource) based on what JARs are on the classpath
* 3. @ComponentScan — scans this package + sub-packages for
* @Component, @Service, @Repository, @Controller, @RestController
*
* When you run main(), Spring Boot:
* 1. Starts embedded Tomcat on port 8080
* 2. Connects to MySQL using application.properties
* 3. Creates/updates the "customers" table via Hibernate
* 4. Registers all REST endpoints from CustomerController
*/@SpringBootApplicationpublic classProducerApp {
public static voidmain(String[] args) {
// This one line bootstraps the entire Spring containerSpringApplication.run(ProducerApp.class, args);
System.out.println("✅ Producer App started on http://localhost:8080");
}
}
☕ bean/CustomerBean.java — Entity (DB Mapping)
package com.demo.producer.bean;
import jakarta.persistence.*;
/**
* @Entity — tells JPA/Hibernate: "This Java class = one row in a database table"
* @Table(name="customers") — the actual table name in MySQL.
* If you skip @Table, the table name defaults to the class name ("CustomerBean").
*
* Hibernate will CREATE TABLE customers (
* id BIGINT PRIMARY KEY AUTO_INCREMENT,
* name VARCHAR(255),
* email VARCHAR(255),
* customer_type VARCHAR(255)
* );
*/@Entity@Table(name = "customers")
public classCustomerBean {
// @Id — this field is the PRIMARY KEY of the table row
// @GeneratedValue(IDENTITY) — MySQL auto-increments this (1, 2, 3, ...)@Id@GeneratedValue(strategy = GenerationType.IDENTITY)
privateLong id;
// @Column — maps this field to "name" column.
// nullable=false means the DB enforces NOT NULL constraint.@Column(name = "name", nullable = false)
privateString name;
// No @Column = Hibernate uses field name "email" as column name by defaultprivateString email;
// Customer category: "GOLD", "SILVER", "PLATINUM"
// This is what the Consumer will filter by@Column(name = "customer_type")
privateString customerType;
// ─── Constructors ───
// JPA requires a no-arg constructor (it creates objects by reflection)publicCustomerBean() {}
publicCustomerBean(String name, String email, String customerType) {
this.name = name;
this.email = email;
this.customerType = customerType;
}
// ─── Getters & Setters ───
// Jackson (JSON library) uses these to convert: Java object ↔ JSONpublicLonggetId() { return id; }
publicStringgetName() { return name; }
publicStringgetEmail() { return email; }
publicStringgetCustomerType() { return customerType; }
public voidsetId(Long id) { this.id = id; }
public voidsetName(String name) { this.name = name; }
public voidsetEmail(String email) { this.email = email; }
public voidsetCustomerType(String customerType) { this.customerType = customerType; }
@OverridepublicStringtoString() {
return"CustomerBean{id=" + id + ", name='" + name + "', type='" + customerType + "'}";
}
}
☕ dao/CustomerDAO.java — Repository (DB Access)
package com.demo.producer.dao;
import com.demo.producer.bean.CustomerBean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @Repository — marks this as a Spring Data DAO (Data Access Object).
*
* JpaRepository<CustomerBean, Long> means:
* - Entity type = CustomerBean (maps to "customers" table)
* - Primary key type = Long (the "id" field)
*
* Spring Data JPA auto-implements this interface at runtime.
* You get FREE methods without writing any SQL:
* - findAll() → SELECT * FROM customers
* - findById(id) → SELECT * FROM customers WHERE id = ?
* - save(entity) → INSERT or UPDATE
* - deleteById(id) → DELETE FROM customers WHERE id = ?
* - count() → SELECT COUNT(*) FROM customers
*/@Repositorypublic interfaceCustomerDAOextendsJpaRepository<CustomerBean, Long> {
// Spring Data JPA generates the SQL from the method name automatically!
// "findBy" + "CustomerType" → WHERE customer_type = ?
// Generated SQL: SELECT * FROM customers WHERE customer_type = 'GOLD'List<CustomerBean> findByCustomerType(String customerType);
// You can also write custom JPQL (not SQL — uses Java class/field names)
// :type is a named parameter, supplied by @Param("type")@Query("SELECT c FROM CustomerBean c WHERE c.customerType = :type ORDER BY c.name ASC")
List<CustomerBean> findByTypeSorted(@Param("type") String type);
}
☕ service/CustomerService.java — Service Interface
package com.demo.producer.service;
import com.demo.producer.bean.CustomerBean;
import java.util.List;
/**
* Why have an interface for the Service?
*
* 1. LOOSE COUPLING: The Controller depends on this interface, not the
* implementation. Tomorrow you can swap CustomerServiceImpl with
* a different implementation without touching the Controller.
*
* 2. TESTING: You can mock this interface in unit tests easily.
*
* 3. SPRING AOP: @Transactional and other Spring proxies work on interfaces.
*
* Rule: Controllers call Service. Service calls DAO. Never skip layers.
*/public interfaceCustomerService {
List<CustomerBean> getAllCustomers();
List<CustomerBean> findByCustomerType(String customerType);
CustomerBeangetCustomerById(Long id);
CustomerBeansaveCustomer(CustomerBean customer);
voiddeleteCustomer(Long id);
}
☕ service/CustomerServiceImpl.java — Business Logic
package com.demo.producer.service;
import com.demo.producer.bean.CustomerBean;
import com.demo.producer.dao.CustomerDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* @Service — marks this class as a Spring service component.
* - Spring creates ONE instance of this (singleton bean).
* - @ComponentScan picks it up and registers it in the Spring container.
* - It can be @Autowired anywhere.
*
* This class contains BUSINESS LOGIC.
* It sits between the Controller (web layer) and DAO (data layer).
*
* Examples of business logic:
* - Checking if a customer already exists before saving
* - Validating business rules (e.g., PLATINUM customers must have 5+ years)
* - Calling multiple DAOs and combining results
*/@Servicepublic classCustomerServiceImplimplementsCustomerService {
// @Autowired — Spring automatically injects the CustomerDAO bean here.
// Spring sees CustomerDAO is a JpaRepository and auto-creates the
// implementation (you don't write any JDBC code).@AutowiredprivateCustomerDAO customerDAO;
@OverridepublicList<CustomerBean> getAllCustomers() {
// findAll() = SELECT * FROM customersreturn customerDAO.findAll();
}
@OverridepublicList<CustomerBean> findByCustomerType(String customerType) {
// Calls our custom finder method → WHERE customer_type = ?return customerDAO.findByCustomerType(customerType);
}
@OverridepublicCustomerBeangetCustomerById(Long id) {
// findById returns Optional<CustomerBean>.
// orElseThrow throws an exception if not found → Spring returns 404.return customerDAO.findById(id)
.orElseThrow(() -> newRuntimeException("Customer not found: " + id));
}
@Override@Transactional// @Transactional — wraps this method in a DB transaction.
// If anything goes wrong inside (exception thrown), the DB change is ROLLED BACK.
// Spring handles the commit/rollback automatically.publicCustomerBeansaveCustomer(CustomerBean customer) {
// save() does INSERT if id is null, UPDATE if id already existsreturn customerDAO.save(customer);
}
@Override@Transactionalpublic voiddeleteCustomer(Long id) {
// DELETE FROM customers WHERE id = ?
customerDAO.deleteById(id);
}
}
package com.demo.producer.controller;
import com.demo.producer.bean.CustomerBean;
import com.demo.producer.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @RestController = @Controller + @ResponseBody
* - Every method's return value is automatically serialized to JSON
* by Jackson (the JSON library bundled with spring-boot-starter-web)
* - No need to add @ResponseBody on each method individually
*
* @RequestMapping("/api/customers") — base URL for all methods in this class.
* All endpoints here start with /api/customers/...
*
* @CrossOrigin — allows requests from other origins (React on :3000,
* Consumer app on :9090, Postman etc.) — prevents CORS errors.
*/@RestController@RequestMapping("/api/customers")
@CrossOriginpublic classCustomerController {
// Inject the Service layer — Controller NEVER talks to DAO directly@AutowiredprivateCustomerService customerService;
// ─── GET /api/customers ─────────────────────────────────────────────
// Returns ALL customers as a JSON array.
// Example response: [{"id":1,"name":"Alice","email":"...","customerType":"GOLD"}, ...]@GetMappingpublicResponseEntity<List<CustomerBean>> getAllCustomers() {
List<CustomerBean> customers = customerService.getAllCustomers();
returnResponseEntity.ok(customers); // 200 OK + JSON body
}
// ─── GET /api/customers/{id} ─────────────────────────────────────────
// Returns one customer by primary key.
// @PathVariable extracts "5" from /api/customers/5 → Long id = 5@GetMapping("/{id}")
publicResponseEntity<CustomerBean> getById(@PathVariableLong id) {
try {
CustomerBean customer = customerService.getCustomerById(id);
returnResponseEntity.ok(customer); // 200 OK
} catch (RuntimeException e) {
returnResponseEntity.notFound().build(); // 404 Not Found
}
}
// ─── GET /api/customers/by-type?customerType=GOLD ────────────────────
// @RequestParam extracts "GOLD" from the query string ?customerType=GOLD
// The Consumer app calls exactly this endpoint!@GetMapping("/by-type")
publicResponseEntity<List<CustomerBean>> getByType(
@RequestParam("customerType") String customerType) {
List<CustomerBean> result = customerService.findByCustomerType(customerType);
if (result.isEmpty()) {
returnResponseEntity.noContent().build(); // 204 No Content
}
returnResponseEntity.ok(result); // 200 OK + JSON array
}
// ─── POST /api/customers ─────────────────────────────────────────────
// Creates a new customer.
// @RequestBody — Spring reads the JSON request body and converts it
// to a CustomerBean object using Jackson.
// Example request body: {"name":"Bob","email":"bob@mail.com","customerType":"SILVER"}@PostMappingpublicResponseEntity<CustomerBean> createCustomer(
@RequestBodyCustomerBean customer) {
CustomerBean saved = customerService.saveCustomer(customer);
// 201 Created (not 200!) — signals a new resource was createdreturnResponseEntity.status(HttpStatus.CREATED).body(saved);
}
// ─── PUT /api/customers/{id} ─────────────────────────────────────────
// Full update — replaces all fields of an existing customer.@PutMapping("/{id}")
publicResponseEntity<CustomerBean> updateCustomer(
@PathVariableLong id,
@RequestBodyCustomerBean customer) {
customer.setId(id); // ensure we update the right rowCustomerBean updated = customerService.saveCustomer(customer);
returnResponseEntity.ok(updated); // 200 OK
}
// ─── DELETE /api/customers/{id} ──────────────────────────────────────
// Deletes a customer by id. Returns 204 No Content on success.
// @ResponseStatus tells Spring to always return 204 for this method.@DeleteMapping("/{id}")
publicResponseEntity<Void> deleteCustomer(@PathVariableLong id) {
customerService.deleteCustomer(id);
returnResponseEntity.noContent().build(); // 204 No Content
}
}
Test Producer with curl or Postman before running Consumer: GET http://localhost:8080/api/customers POST http://localhost:8080/api/customers with body {"name":"Alice","email":"alice@mail.com","customerType":"GOLD"}
05 — Consumer App Code
🖥 Consumer App port 9090
“This app has NO database. It calls the Producer's API using RestTemplate and processes the data.”
📄 consumer-app/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
Consumer pom.xml — Much simpler than Producer!
We ONLY need:
- spring-boot-starter-web → gives us RestTemplate + Jackson (JSON converter)
We do NOT need JPA or MySQL because this app has no database.
-->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>consumer-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<!--
spring-boot-starter-web gives us:
- RestTemplate (HTTP client to call Producer)
- Jackson (JSON ↔ Java conversion)
- Embedded Tomcat (even though we don't serve web pages,
Spring Boot needs a web context to run CommandLineRunner properly)
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
# ─────────────────────────────────────────────────────
# Consumer App Configuration
# NOTICE: No datasource config at all — no DB needed!
# ─────────────────────────────────────────────────────# Consumer runs on port 9090 — Producer is on 8080, so they don't conflict
server.port=9090
# The base URL of the Producer app.
# All RestTemplate calls will use this as the starting point.
# If Producer were deployed to a cloud server, this would be its IP/domain.
producer.base.url=http://localhost:8080/api/customers
# Reduce noisy log output to just show our print statements
logging.level.root=WARN
logging.level.com.demo.consumer=INFO
☕ config/AppConfig.java — RestTemplate Bean
package com.demo.consumer.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @Configuration — this class defines Spring beans.
* Spring calls @Bean methods once and puts the result in the Spring container.
* Any class that needs RestTemplate can then @Autowire it.
*
* Why define RestTemplate as a @Bean instead of doing "new RestTemplate()"?
*
* 1. SINGLETON: Spring creates one instance and reuses it everywhere — efficient.
* 2. TESTABLE: You can mock or replace it easily in tests.
* 3. CONFIGURABLE: You can add interceptors, timeouts, error handlers to the
* bean centrally without touching every class that uses it.
*
* RestTemplate is Spring's synchronous HTTP client:
* - Makes GET/POST/PUT/DELETE calls to external REST APIs
* - Automatically converts JSON response → Java object (via Jackson)
* - Automatically converts Java object → JSON for request body
*/@Configurationpublic classAppConfig {
@BeanpublicRestTemplaterestTemplate() {
// Simple RestTemplate with default settings.
// For production you'd add connection timeouts:
// HttpComponentsClientHttpRequestFactory factory = new ...
// factory.setConnectTimeout(5000);
// return new RestTemplate(factory);return newRestTemplate();
}
}
☕ bean/CustomerBean.java — Plain POJO (Mirror of Producer)
package com.demo.consumer.bean;
/**
* This is a PLAIN Java class — NO @Entity, NO JPA annotations.
* The Consumer has no database, so it doesn't need JPA mapping.
*
* This class exists ONLY to hold the JSON data that comes back
* from the Producer's API. Jackson (the JSON library) needs a POJO
* with matching field names to deserialise the JSON into.
*
* When RestTemplate gets this JSON from Producer:
* {"id":1, "name":"Alice", "email":"alice@mail.com", "customerType":"GOLD"}
*
* Jackson creates: new CustomerBean(), then calls:
* setId(1L), setName("Alice"), setEmail("alice@mail.com"), setCustomerType("GOLD")
*
* The field names must MATCH the JSON keys exactly (case-sensitive).
*/public classCustomerBean {
privateLong id;
privateString name;
privateString email;
privateString customerType;
publicCustomerBean() {} // Required by Jackson for deserialisationpublicLonggetId() { return id; }
publicStringgetName() { return name; }
publicStringgetEmail() { return email; }
publicStringgetCustomerType() { return customerType; }
public voidsetId(Long id) { this.id = id; }
public voidsetName(String name) { this.name = name; }
public voidsetEmail(String email) { this.email = email; }
public voidsetCustomerType(String t) { this.customerType = t; }
@OverridepublicStringtoString() {
return"CustomerBean{id=" + id + ", name='" + name +
"', email='" + email + "', type='" + customerType + "'}";
}
}
☕ service/CustomerConsumerService.java — HTTP Calls via RestTemplate
package com.demo.consumer.service;
import com.demo.consumer.bean.CustomerBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.List;
/**
* This service makes HTTP calls to the Producer app using RestTemplate.
*
* @Value("${producer.base.url}") — reads the URL from application.properties.
* This is dependency injection for config values. Much better than hardcoding URLs.
*
* RestTemplate methods used:
* getForObject(url, ResponseType.class) → GET, returns body directly
* getForEntity(url, ResponseType.class) → GET, returns ResponseEntity (body+status)
* postForObject(url, requestBody, ResponseType.class) → POST
* put(url, requestBody) → PUT (no return value)
* delete(url) → DELETE (no return value)
* exchange(url, method, httpEntity, ResponseType.class) → any verb, most control
*/@Servicepublic classCustomerConsumerService {
@AutowiredprivateRestTemplate restTemplate;
// Reads "producer.base.url" from application.properties
// Result: baseUrl = "http://localhost:8080/api/customers"@Value("${producer.base.url}")
privateString baseUrl;
// ─── 1. GET ALL CUSTOMERS ─────────────────────────────────────────────
// getForObject returns the JSON body directly as a Java array.
// Why array (CustomerBean[]) and not List?
// Because Java generics are erased at runtime — RestTemplate can't tell
// List<CustomerBean> from List<Something>. Arrays don't have this problem.
// We convert the array to a List using Arrays.asList() after.publicList<CustomerBean> getAllCustomers() {
System.out.println("📡 Calling: GET " + baseUrl);
CustomerBean[] arr = restTemplate.getForObject(baseUrl, CustomerBean[].class);
// restTemplate.getForObject:
// 1. Makes HTTP GET request to "http://localhost:8080/api/customers"
// 2. Producer returns JSON array: [{"id":1,"name":"Alice",...}, ...]
// 3. Jackson converts JSON array → CustomerBean[] Java array
// 4. Returns the array to usreturnArrays.asList(arr);
}
// ─── 2. GET CUSTOMERS BY TYPE ─────────────────────────────────────────
// URL: http://localhost:8080/api/customers/by-type?customerType=GOLDpublicList<CustomerBean> getByType(String type) {
String url = baseUrl + "/by-type?customerType=" + type;
System.out.println("📡 Calling: GET " + url);
CustomerBean[] arr = restTemplate.getForObject(url, CustomerBean[].class);
return arr != null ? Arrays.asList(arr) : List.of();
}
// ─── 3. GET ONE CUSTOMER BY ID ────────────────────────────────────────
// getForEntity — like getForObject but also gives you the status code.
// Useful when you want to check if response was 200, 404, etc.publicCustomerBeangetById(Long id) {
String url = baseUrl + "/" + id;
System.out.println("📡 Calling: GET " + url);
ResponseEntity<CustomerBean> response =
restTemplate.getForEntity(url, CustomerBean.class);
// response.getStatusCode() → HttpStatus.OK (200)
// response.getBody() → the CustomerBean objectSystem.out.println(" ↳ Status: " + response.getStatusCode());
return response.getBody();
}
// ─── 4. CREATE A NEW CUSTOMER (POST) ─────────────────────────────────
// postForObject:
// 1. Converts our CustomerBean Java object → JSON string using Jackson
// 2. Sends HTTP POST with that JSON as the request body
// 3. Producer receives it, saves to DB, returns saved object as JSON
// 4. Jackson converts response JSON → CustomerBean Java objectpublicCustomerBeancreateCustomer(CustomerBean customer) {
System.out.println("📡 Calling: POST " + baseUrl + " with body: " + customer);
CustomerBean saved = restTemplate.postForObject(baseUrl, customer, CustomerBean.class);
System.out.println(" ↳ Created: " + saved);
return saved;
}
// ─── 5. UPDATE CUSTOMER (PUT) ─────────────────────────────────────────
// put() sends a PUT request — no return value.
// If you need the updated object back, use exchange() instead.public voidupdateCustomer(Long id, CustomerBean customer) {
String url = baseUrl + "/" + id;
System.out.println("📡 Calling: PUT " + url);
restTemplate.put(url, customer); // no response bodySystem.out.println(" ↳ Updated customer id=" + id);
}
// ─── 6. DELETE CUSTOMER ───────────────────────────────────────────────
// delete() sends DELETE request — no return value.public voiddeleteCustomer(Long id) {
String url = baseUrl + "/" + id;
System.out.println("📡 Calling: DELETE " + url);
restTemplate.delete(url);
System.out.println(" ↳ Deleted customer id=" + id);
}
// ─── 7. ADVANCED: exchange() ─────────────────────────────────────────
// exchange() gives you full control: custom headers, any HTTP method,
// full ResponseEntity back. Used when you need:
// - Custom headers (e.g., Authorization: Bearer token)
// - POST and get full ResponseEntity (not just body)publicResponseEntity<CustomerBean> createWithHeaders(CustomerBean customer) {
HttpHeaders headers = newHttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// headers.set("Authorization", "Bearer my-token"); // for secured APIsHttpEntity<CustomerBean> request = newHttpEntity<>(customer, headers);
return restTemplate.exchange(
baseUrl, // URLHttpMethod.POST, // HTTP verb
request, // body + headersCustomerBean.class // expected response type
);
// Returns ResponseEntity<CustomerBean> — you can check:
// response.getStatusCode() → HttpStatus.CREATED (201)
// response.getHeaders() → all response headers
// response.getBody() → the saved CustomerBean
}
}
☕ runner/AppRunner.java — Runs on Startup
package com.demo.consumer.runner;
import com.demo.consumer.bean.CustomerBean;
import com.demo.consumer.service.CustomerConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* CommandLineRunner — a Spring Boot interface with one method: run(String... args).
*
* Spring Boot automatically calls run() AFTER the Spring container is fully
* initialised (all @Autowired dependencies are injected, all beans are ready).
*
* This is perfect for:
* - Running startup tasks (like calling a REST API on boot)
* - Batch jobs that run once and exit
* - Console/CLI applications that don't serve HTTP requests
*
* @Component — registers this class as a Spring bean so Spring picks it up
* and calls its run() method automatically.
*
* HOW TO USE:
* 1. Start Producer app first (port 8080)
* 2. Insert some test data via Postman:
* POST http://localhost:8080/api/customers
* Body: {"name":"Alice","email":"alice@mail.com","customerType":"GOLD"}
* 3. Start Consumer app (port 9090) — it will call Producer and print results
*/@Componentpublic classAppRunnerimplementsCommandLineRunner {
@AutowiredprivateCustomerConsumerService consumerService;
@Overridepublic voidrun(String... args) throwsException {
System.out.println("\n╔══════════════════════════════════════════╗");
System.out.println("║ CONSUMER APP — Calling Producer API ║");
System.out.println("╚══════════════════════════════════════════╝\n");
// ─── STEP 1: Create a few test customers via POST ─────────────────System.out.println("── Step 1: Creating test customers ──");
CustomerBean c1 = newCustomerBean();
c1.setName("Alice Johnson");
c1.setEmail("alice@mail.com");
c1.setCustomerType("GOLD");
CustomerBean saved1 = consumerService.createCustomer(c1);
CustomerBean c2 = newCustomerBean();
c2.setName("Bob Smith");
c2.setEmail("bob@mail.com");
c2.setCustomerType("SILVER");
CustomerBean saved2 = consumerService.createCustomer(c2);
CustomerBean c3 = newCustomerBean();
c3.setName("Carol White");
c3.setEmail("carol@mail.com");
c3.setCustomerType("GOLD");
CustomerBean saved3 = consumerService.createCustomer(c3);
// ─── STEP 2: Fetch ALL customers ──────────────────────────────────System.out.println("\n── Step 2: Fetch ALL customers ──");
List<CustomerBean> all = consumerService.getAllCustomers();
all.forEach(c -> System.out.println(" → " + c));
// ─── STEP 3: Filter by GOLD type ──────────────────────────────────System.out.println("\n── Step 3: Filter by customerType=GOLD ──");
List<CustomerBean> golds = consumerService.getByType("GOLD");
golds.forEach(c -> System.out.println(" 🥇 " + c.getName() + " → " + c.getEmail()));
// ─── STEP 4: Get one by ID ─────────────────────────────────────────System.out.println("\n── Step 4: Get customer by ID=" + saved1.getId() + " ──");
CustomerBean fetched = consumerService.getById(saved1.getId());
System.out.println(" → " + fetched);
// ─── STEP 5: Update a customer ────────────────────────────────────System.out.println("\n── Step 5: Update Bob to PLATINUM ──");
saved2.setCustomerType("PLATINUM");
saved2.setEmail("bob.updated@mail.com");
consumerService.updateCustomer(saved2.getId(), saved2);
// ─── STEP 6: Verify the update ────────────────────────────────────System.out.println("\n── Step 6: Verify updated Bob ──");
CustomerBean updatedBob = consumerService.getById(saved2.getId());
System.out.println(" → " + updatedBob);
// ─── STEP 7: Delete a customer ────────────────────────────────────System.out.println("\n── Step 7: Delete Carol ──");
consumerService.deleteCustomer(saved3.getId());
// ─── STEP 8: Final list ───────────────────────────────────────────System.out.println("\n── Step 8: Final customer list ──");
consumerService.getAllCustomers()
.forEach(c -> System.out.println(" → " + c));
System.out.println("\n✅ Consumer App finished. Check Producer logs for SQL.");
}
}
☕ ConsumerApp.java — Main Class
package com.demo.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Consumer app entry point.
*
* When this runs:
* 1. Spring container starts
* 2. All @Autowired dependencies are injected
* 3. Spring Boot calls AppRunner.run() automatically (because AppRunner
* implements CommandLineRunner and is a @Component)
* 4. AppRunner calls the Producer's REST API using RestTemplate
* 5. Results print to the console
* 6. App stays running (Tomcat is embedded) — you can shut it down with Ctrl+C
*
* KEY POINT: You start Producer FIRST (port 8080), THEN Consumer (port 9090).
* If Producer isn't running, Consumer will throw a connection refused error.
*/@SpringBootApplicationpublic classConsumerApp {
public static voidmain(String[] args) {
SpringApplication.run(ConsumerApp.class, args);
}
}
06 — Running the Demo
How to Run Step by Step
“Follow these steps exactly. Order matters — Producer must be alive before Consumer starts.”
1
Create the MySQL database
Open MySQL Workbench or terminal and run: CREATE DATABASE customerdb;
2
Import Producer app into IDE
Open producer-app/ as a separate Maven project. Update application.properties with your MySQL username and password.
3
Run ProducerApp.java
Right-click → Run As → Java Application. Wait until you see Tomcat started on port 8080 in the console. The customers table is auto-created by Hibernate.
4
Test Producer with Postman (optional)
Try GET http://localhost:8080/api/customers — should return an empty JSON array []. This confirms the Producer is running.
5
Import Consumer app into IDE (separate project)
Open consumer-app/ as another Maven project in the same IDE or a new window.
6
Run ConsumerApp.java
Right-click → Run As → Java Application. Watch the console — you'll see HTTP calls being made and data printed. Check the Producer console to see the SQL statements Hibernate ran.
Expected Consumer Console Output
╔══════════════════════════════════════════╗
║ CONSUMER APP — Calling Producer API ║
╚══════════════════════════════════════════╝
── Step 1: Creating test customers ──
📡 Calling: POST http://localhost:8080/api/customers with body: CustomerBean{id=null, name='Alice Johnson', type='GOLD'}
↳ Created: CustomerBean{id=1, name='Alice Johnson', type='GOLD'}
📡 Calling: POST http://localhost:8080/api/customers with body: CustomerBean{id=null, name='Bob Smith', type='SILVER'}
↳ Created: CustomerBean{id=2, name='Bob Smith', type='SILVER'}
...
── Step 2: Fetch ALL customers ──
📡 Calling: GET http://localhost:8080/api/customers
→ CustomerBean{id=1, name='Alice Johnson', type='GOLD'}
→ CustomerBean{id=2, name='Bob Smith', type='SILVER'}
→ CustomerBean{id=3, name='Carol White', type='GOLD'}
── Step 3: Filter by customerType=GOLD ──
📡 Calling: GET http://localhost:8080/api/customers/by-type?customerType=GOLD
🥇 Alice Johnson → alice@mail.com
🥇 Carol White → carol@mail.com
...
✅ Consumer App finished. Check Producer logs for SQL.
⚠ If Consumer throws Connection Refused
This means the Producer is not running. Make sure ProducerApp.java is running on port 8080 before you start the Consumer. Also ensure no firewall is blocking localhost connections.
07 — Request Trace
What Happens When Consumer calls getByType("GOLD")
Makes HTTP GET to http://localhost:8080/api/customers/by-type?customerType=GOLD
↓ Network (HTTP over TCP)
3. Tomcat (Producer :8080)
Receives the HTTP GET request, routes to DispatcherServlet
↓
4. CustomerController.getByType()
@RequestParam("customerType") extracts "GOLD" from query string
↓
5. CustomerServiceImpl.findByCustomerType("GOLD")
Business layer delegates to DAO
↓
6. CustomerDAO.findByCustomerType("GOLD")
Hibernate generates: SELECT * FROM customers WHERE customer_type='GOLD'
↓
7. MySQL executes query
Returns 2 rows (Alice, Carol) as a ResultSet
↓
8. Jackson serialises result
Converts List<CustomerBean> → JSON array string
↓ HTTP 200 OK + JSON body
9. RestTemplate receives JSON
Jackson converts JSON array → CustomerBean[] Java array in Consumer
↓
10. AppRunner prints results
🥇 Alice Johnson, 🥇 Carol White printed to console
08 — Interview Q&A
Microservices Interview Questions
“Questions specifically about microservices architecture”
What is the difference between a Monolith and Microservices?
A Monolith is one large application — all layers (web, business, database) in one JAR deployed together. If any part crashes, the whole app is down. Microservices break the app into small, independent services. Each service has its own codebase, can use its own database, is deployed independently, and communicates with others via REST/HTTP. In this demo: Producer and Consumer are two separate Spring Boot apps.
What is a Producer in microservices?
The Producer (also called a Provider or Service) is the app that OWNS the data and exposes REST API endpoints. Other apps call it to get or modify data. In our demo: the Producer app owns the customers table in MySQL and exposes /api/customers endpoints. The Producer doesn't know or care who calls it — it just handles HTTP requests and returns JSON.
What is a Consumer in microservices?
The Consumer is an app that calls another service's REST API to use its data. It does NOT have its own database for that data. Instead it fetches data over HTTP from the Producer when needed. In our demo: the Consumer uses RestTemplate to call the Producer's API. The Consumer could be a mobile backend, an analytics service, a report generator, etc.
Why does the Consumer have its own CustomerBean class without @Entity?
The Consumer has no database, so it doesn't need @Entity (which is a JPA annotation for DB mapping). It needs a plain POJO (Plain Old Java Object) that mirrors the JSON structure returned by the Producer. Jackson (the JSON library) uses this POJO's setter methods to populate it from the JSON response. The field names must match the JSON keys exactly.
What is CommandLineRunner? Why use it in the Consumer?
CommandLineRunner is a Spring Boot interface with one method: run(String... args). Spring Boot automatically calls run() after the Spring context is fully initialized. It's used for code that should execute once at startup — batch jobs, initialization, CLI tasks. In our Consumer, run() calls the Producer's REST API immediately when the app starts, demonstrating the producer-consumer communication.
How do two microservices share data without a shared database?
Each service owns its own database (Database per Service pattern). They share data by calling each other's REST APIs. In our demo, the Consumer doesn't access MySQL directly — it asks the Producer "give me GOLD customers" via HTTP, and the Producer queries its DB and returns JSON. This keeps services decoupled: Consumer doesn't need to know what database Producer uses.
What happens if the Producer is down when Consumer calls it?
RestTemplate throws a ResourceAccessException (wrapping a ConnectException). The Consumer app would crash unless you handle this. In production microservices, you'd use: (1) Circuit Breaker pattern (Resilience4j) to fail gracefully, (2) Retry logic, (3) Fallback response. This is a key challenge of microservices — distributed failure handling.
Why run the Producer on port 8080 and Consumer on 9090?
Both apps embed Tomcat and would conflict if they used the same port on the same machine. In production, they'd run on different servers/containers (Docker), so port conflicts wouldn't exist. Locally, we change the port in application.properties: server.port=9090 for the Consumer. This simulates running on different machines.