⌂ Home
Microservices · Spring Boot

Microservices Producer & Consumer

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 Apps REST API (Producer) RestTemplate (Consumer) Spring Data JPA MySQL Database CommandLineRunner
Consumer App HTTP GET/POST Producer API Service Layer JPA Repository MySQL DB

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.

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.

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).

🌐 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/src/main/resources/application.properties
# ───────────────────────────────────────────────── # 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 */ @SpringBootApplication public class ProducerApp { public static void main(String[] args) { // This one line bootstraps the entire Spring container SpringApplication.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 class CustomerBean { // @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) private Long id; // @Column — maps this field to "name" column. // nullable=false means the DB enforces NOT NULL constraint. @Column(name = "name", nullable = false) private String name; // No @Column = Hibernate uses field name "email" as column name by default private String email; // Customer category: "GOLD", "SILVER", "PLATINUM" // This is what the Consumer will filter by @Column(name = "customer_type") private String customerType; // ─── Constructors ─── // JPA requires a no-arg constructor (it creates objects by reflection) public CustomerBean() {} public CustomerBean(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 ↔ JSON public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } public String getCustomerType() { return customerType; } public void setId(Long id) { this.id = id; } public void setName(String name) { this.name = name; } public void setEmail(String email) { this.email = email; } public void setCustomerType(String customerType) { this.customerType = customerType; } @Override public String toString() { 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 */ @Repository public interface CustomerDAO extends JpaRepository<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 interface CustomerService { List<CustomerBean> getAllCustomers(); List<CustomerBean> findByCustomerType(String customerType); CustomerBean getCustomerById(Long id); CustomerBean saveCustomer(CustomerBean customer); void deleteCustomer(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 */ @Service public class CustomerServiceImpl implements CustomerService { // @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). @Autowired private CustomerDAO customerDAO; @Override public List<CustomerBean> getAllCustomers() { // findAll() = SELECT * FROM customers return customerDAO.findAll(); } @Override public List<CustomerBean> findByCustomerType(String customerType) { // Calls our custom finder method → WHERE customer_type = ? return customerDAO.findByCustomerType(customerType); } @Override public CustomerBean getCustomerById(Long id) { // findById returns Optional<CustomerBean>. // orElseThrow throws an exception if not found → Spring returns 404. return customerDAO.findById(id) .orElseThrow(() -> new RuntimeException("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. public CustomerBean saveCustomer(CustomerBean customer) { // save() does INSERT if id is null, UPDATE if id already exists return customerDAO.save(customer); } @Override @Transactional public void deleteCustomer(Long id) { // DELETE FROM customers WHERE id = ? customerDAO.deleteById(id); } }
controller/CustomerController.java — REST Endpoints
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") @CrossOrigin public class CustomerController { // Inject the Service layer — Controller NEVER talks to DAO directly @Autowired private CustomerService customerService; // ─── GET /api/customers ───────────────────────────────────────────── // Returns ALL customers as a JSON array. // Example response: [{"id":1,"name":"Alice","email":"...","customerType":"GOLD"}, ...] @GetMapping public ResponseEntity<List<CustomerBean>> getAllCustomers() { List<CustomerBean> customers = customerService.getAllCustomers(); return ResponseEntity.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}") public ResponseEntity<CustomerBean> getById(@PathVariable Long id) { try { CustomerBean customer = customerService.getCustomerById(id); return ResponseEntity.ok(customer); // 200 OK } catch (RuntimeException e) { return ResponseEntity.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") public ResponseEntity<List<CustomerBean>> getByType( @RequestParam("customerType") String customerType) { List<CustomerBean> result = customerService.findByCustomerType(customerType); if (result.isEmpty()) { return ResponseEntity.noContent().build(); // 204 No Content } return ResponseEntity.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"} @PostMapping public ResponseEntity<CustomerBean> createCustomer( @RequestBody CustomerBean customer) { CustomerBean saved = customerService.saveCustomer(customer); // 201 Created (not 200!) — signals a new resource was created return ResponseEntity.status(HttpStatus.CREATED).body(saved); } // ─── PUT /api/customers/{id} ───────────────────────────────────────── // Full update — replaces all fields of an existing customer. @PutMapping("/{id}") public ResponseEntity<CustomerBean> updateCustomer( @PathVariable Long id, @RequestBody CustomerBean customer) { customer.setId(id); // ensure we update the right row CustomerBean updated = customerService.saveCustomer(customer); return ResponseEntity.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}") public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) { customerService.deleteCustomer(id); return ResponseEntity.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"}

🖥 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/src/main/resources/application.properties
# ───────────────────────────────────────────────────── # 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 */ @Configuration public class AppConfig { @Bean public RestTemplate restTemplate() { // Simple RestTemplate with default settings. // For production you'd add connection timeouts: // HttpComponentsClientHttpRequestFactory factory = new ... // factory.setConnectTimeout(5000); // return new RestTemplate(factory); return new RestTemplate(); } }
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 class CustomerBean { private Long id; private String name; private String email; private String customerType; public CustomerBean() {} // Required by Jackson for deserialisation public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } public String getCustomerType() { return customerType; } public void setId(Long id) { this.id = id; } public void setName(String name) { this.name = name; } public void setEmail(String email) { this.email = email; } public void setCustomerType(String t) { this.customerType = t; } @Override public String toString() { 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 */ @Service public class CustomerConsumerService { @Autowired private RestTemplate restTemplate; // Reads "producer.base.url" from application.properties // Result: baseUrl = "http://localhost:8080/api/customers" @Value("${producer.base.url}") private String 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. public List<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 us return Arrays.asList(arr); } // ─── 2. GET CUSTOMERS BY TYPE ───────────────────────────────────────── // URL: http://localhost:8080/api/customers/by-type?customerType=GOLD public List<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. public CustomerBean getById(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 object System.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 object public CustomerBean createCustomer(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 void updateCustomer(Long id, CustomerBean customer) { String url = baseUrl + "/" + id; System.out.println("📡 Calling: PUT " + url); restTemplate.put(url, customer); // no response body System.out.println(" ↳ Updated customer id=" + id); } // ─── 6. DELETE CUSTOMER ─────────────────────────────────────────────── // delete() sends DELETE request — no return value. public void deleteCustomer(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) public ResponseEntity<CustomerBean> createWithHeaders(CustomerBean customer) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // headers.set("Authorization", "Bearer my-token"); // for secured APIs HttpEntity<CustomerBean> request = new HttpEntity<>(customer, headers); return restTemplate.exchange( baseUrl, // URL HttpMethod.POST, // HTTP verb request, // body + headers CustomerBean.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 */ @Component public class AppRunner implements CommandLineRunner { @Autowired private CustomerConsumerService consumerService; @Override public void run(String... args) throws Exception { 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 = new CustomerBean(); c1.setName("Alice Johnson"); c1.setEmail("alice@mail.com"); c1.setCustomerType("GOLD"); CustomerBean saved1 = consumerService.createCustomer(c1); CustomerBean c2 = new CustomerBean(); c2.setName("Bob Smith"); c2.setEmail("bob@mail.com"); c2.setCustomerType("SILVER"); CustomerBean saved2 = consumerService.createCustomer(c2); CustomerBean c3 = new CustomerBean(); 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. */ @SpringBootApplication public class ConsumerApp { public static void main(String[] args) { SpringApplication.run(ConsumerApp.class, args); } }

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.

What Happens When Consumer calls getByType("GOLD")

“Every hop through both apps, step by step”

1. AppRunner.run()
Consumer App calls consumerService.getByType("GOLD")
2. RestTemplate.getForObject()
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

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.
↑ Back to top