Definition
- API Gateway – the single entry point that handles routing, authentication, and rate limiting so clients never talk to services directly.
- Service Registry – a directory (Eureka or Consul) where services register under a logical name and callers resolve that name instead of hardcoding addresses.
- Config Server – centralizes externalized configuration so you change a timeout once without rebuilding any service.
- Inter-service communication – synchronous (REST, Feign, gRPC) when the caller needs an answer, asynchronous (Kafka, RabbitMQ) when it should not wait.
- Database-per-service – each service owns a private data store; no other service touches its tables.
- Circuit breaker – wraps remote calls and fails fast when a dependency is unhealthy, stopping cascading failures.
Think of National Bank of India as a large campus. The API Gateway is the main reception that checks your ID and directs you. The Service Registry is the campus directory that tells reception which building the Payment department is in today, because departments move. The Config Server is the central rulebook every department reads from. The load balancer is the queue manager sending you to whichever teller counter is free. Monitoring is the CCTV and alarm room. The circuit breaker is the "counter temporarily closed" sign so customers do not pile up at a desk that cannot help them.
Understanding the Concepts
What is an API Gateway?
The API Gateway is the single public entry point for all client requests. It handles routing, authentication, rate limiting, and request aggregation so these cross-cutting concerns live in one place instead of being duplicated in every service.
package com.bank.gateway;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayRoutesConfig {
@Bean
public RouteLocator bankRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("payment-service", r -> r
.path("/api/v1/payments/**")
.filters(f -> f
.circuitBreaker(c -> c.setName("paymentCB"))
.requestRateLimiter(l -> l.setRateLimiter(redisRateLimiter())))
.uri("lb://payment-service"))
.route("account-service", r -> r
.path("/api/v1/accounts/**")
.uri("lb://account-service"))
.build();
}
}
Expected behavior:
GET /api/v1/payments/PAY-001 -> routed to payment-service instance
GET /api/v1/accounts/SAV2001 -> routed to account-service instance
The lb:// prefix means load-balanced lookup through the registry.
What is a Service Registry?
The Service Registry solves the problem that service instances come and go with scaling and deploys, so you cannot hardcode addresses. Each service registers itself on startup, and callers resolve the logical name.
package com.bank.account;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class AccountServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AccountServiceApplication.class, args);
}
}
Eureka dashboard:
account-service -> 192.168.1.10:8081, 192.168.1.11:8081 (2 instances)
payment-service -> 192.168.1.20:8082 (1 instance)
notification-service -> 192.168.1.30:8083 (1 instance)
What is a Config Server?
The Config Server externalizes configuration so services read settings from one central, Git-backed server. You change a timeout once without rebuilding any service.
# application-payment-service.yml (stored in Git, served by Config Server)
spring:
config:
import: configserver:http://config-server:8888
account-service:
timeout: 3000
retry:
max-attempts: 3
fraud-check:
enabled: true
threshold: 100000.00
Expected behavior:
Payment Service starts -> pulls config from config-server:8888
Changing account-service.timeout to 5000 in Git
-> All payment-service instances pick up new value on refresh.
How does inter-service communication work?
Synchronous communication uses a declarative client like Feign where the caller needs an immediate answer. Asynchronous communication uses Kafka where the caller should not wait.
package com.bank.payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.math.BigDecimal;
// Synchronous: Feign client resolves account-service via registry
@FeignClient(name = "account-service")
public interface AccountClient {
@PostMapping("/api/v1/accounts/{id}/debit")
void debit(@PathVariable String id, @RequestBody DebitRequest request);
@PostMapping("/api/v1/accounts/{id}/credit")
void credit(@PathVariable String id, @RequestBody CreditRequest request);
}
package com.bank.payment;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
// Asynchronous: publish event so Notification Service reacts later
@Service
public class PaymentEventPublisher {
private final KafkaTemplate<String, PaymentEvent> kafkaTemplate;
public PaymentEventPublisher(KafkaTemplate<String, PaymentEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishTransferCompleted(String paymentId, PaymentEvent event) {
kafkaTemplate.send("payment-events", paymentId, event);
}
}
Expected output:
Synchronous: POST account-service/api/v1/accounts/SAV2001/debit -> 200 OK
Asynchronous: payment-events topic receives PaymentEvent for PAY-001
What is Database-per-Service?
Each service owns its tables and no other service connects to them. If the Payment Service needs the holder's name, it calls the Account Service or consumes events into a local read model.
account-service database: ┌─────────────────────────────────────┐ │ accounts │ │ id | holder_name | balance | type │ │ SAV2001 | Rajesh | 500000.00 | SAV │ │ SAV2002 | Priya | 250000.00 | SAV │ └─────────────────────────────────────┘ payment-service database: ┌───────────────────────────────────────────┐ │ payments │ │ id | from_account | to_account | amount │ │ PAY-001 | SAV2001 | SAV2002 | 50000.00 │ └───────────────────────────────────────────┘ No cross-service joins. No shared schema.
What is a Circuit Breaker?
A circuit breaker wraps remote calls and tracks the failure rate over a sliding window. When failures cross the threshold, it flips from CLOSED to OPEN and every call fails instantly by invoking the fallback.
package com.bank.resilience;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.logging.Logger;
@Service
public class ResilientAccountClient {
private static final Logger LOG = Logger.getLogger(ResilientAccountClient.class.getName());
@CircuitBreaker(name = "accountService", fallbackMethod = "debitFallback")
@Retry(name = "accountService")
@Bulkhead(name = "accountService")
public void debit(String accountId, BigDecimal amount) {
// restTemplate.postForEntity("http://account-service/...")
LOG.info("Debit completed for account: " + accountId);
}
public void debitFallback(String accountId, BigDecimal amount, Throwable t) {
LOG.warning("Circuit OPEN: queuing debit for " + accountId);
}
}
Circuit Breaker state transitions:
CLOSED (normal) -> failure rate > 50% -> OPEN (fail fast)
OPEN -> wait 60s -> HALF_OPEN (test a few calls)
HALF_OPEN -> calls succeed -> CLOSED
HALF_OPEN -> calls fail -> OPEN
How does observability work?
Observability ties the system together with metrics, logs, and traces. Prometheus scrapes metrics, Grafana dashboards them, ELK aggregates structured logs, and Zipkin with a shared traceId shows the full request waterfall.
package com.bank.observability;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class TransferMetrics {
private final Counter transfersInitiated;
private final Counter transfersCompleted;
private final Counter transfersFailed;
public TransferMetrics(MeterRegistry registry) {
this.transfersInitiated = registry.counter("transfers.initiated");
this.transfersCompleted = registry.counter("transfers.completed");
this.transfersFailed = registry.counter("transfers.failed");
}
public void recordInitiated() { transfersInitiated.increment(); }
public void recordCompleted() { transfersCompleted.increment(); }
public void recordFailed() { transfersFailed.increment(); }
}
Expected Prometheus metrics:
transfers_initiated_total 1523
transfers_completed_total 1500
transfers_failed_total 23
How do these components work together?
None of these components are optional extras. Without a registry you get brittle addresses, without a config server you get config drift, and without circuit breakers one slow dependency takes the whole system down.
Request flow through all components:
Client -> API Gateway (auth, rate limit)
|
v
Service Registry (resolve payment-service)
|
v
Load Balancer (pick healthy instance)
|
v
Payment Service
|
+--[Feign + CircuitBreaker]--> Account Service
| |
+--[Kafka]--> Notification Service |
| |
Payment DB Account DB
|
Micrometer -> Prometheus -> Grafana (metrics)
Logback -> ELK (logs)
Micrometer Tracing -> Zipkin (traces)
What are common mistakes with these components?
The most common mistake is treating these components as optional.
package com.bank.antipattern;
// WRONG: Hardcoded service URL. Breaks when instances change.
public class WrongAccountClient {
private static final String URL = "http://192.168.1.10:8081/api/v1/accounts";
// If this instance goes down or IP changes, everything breaks.
}
// CORRECT: Use service discovery with logical name.
// @FeignClient(name = "account-service")
// The registry resolves the name to healthy instances.
Visual Overview
┌─────────────────────────────────────────────────┐
│ CLIENT REQUEST │
└───────────────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ API GATEWAY │
│ Auth │ Rate Limit │ Routing │ Circuit Breaker │
└───────────────────────┬─────────────────────────┘
│ lb://payment-service
▼
┌──────────────┐ resolves ┌──────────────────┐
│ SERVICE │───────────►│ LOAD BALANCER │
│ REGISTRY │ │ (pick healthy) │
└──────────────┘ └────────┬─────────┘
│
┌────────────┴────────────┐
▼ ▼
┌────────────────┐ ┌────────────────┐
│ Payment Svc │ │ Payment Svc │
│ Instance 1 │ │ Instance 2 │
└───────┬────────┘ └────────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Account Svc │ │ Kafka Topic │
│ (Feign+CB) │ │ payment-evts │
└──────┬──────┘ └──────┬───────┘
│ │
┌──────┴──────┐ ┌──────┴───────┐
│ Account DB │ │ Notification │
└─────────────┘ │ Service │
└──────────────┘
Key Takeaway
A microservices architecture is assembled from a standard set of components, and in interviews I describe each by the problem it solves. The API Gateway is the single public entry point that handles routing, authentication, and rate limiting so these cross-cutting concerns live in one place instead of being copied into every service. The Service Registry solves the problem that instances come and go with scaling and deploys. The Config Server centralizes externalized configuration, and inter-service communication comes in two flavors: synchronous with REST or Feign when the caller needs an answer, and asynchronous with Kafka when it should not wait.
Database-per-service keeps each service's data private, so the Payment Service at National Bank of India never reaches into the Account Service's tables. A load balancer spreads traffic across healthy instances, and circuit breakers in Resilience4j fail fast so one slow dependency does not take the whole system down. Observability ties everything together with Prometheus for metrics, ELK for structured logs, and Zipkin with a shared traceId so Pankaj can follow one of Rajesh's transfers across the gateway, payment, and account services.
The point I always make to an interviewer is that none of these are optional luxuries. Without a registry you get brittle addresses, without a config server you get config drift, and without circuit breakers one failing dependency cascades into a full outage.
Remember one sentence: a production microservices architecture needs an API Gateway for routing, a Service Registry for discovery, a Config Server for configuration, circuit breakers for resilience, and observability for tracing, and none of them are optional.
---