Definition
- API Gateway – the single secured front door that terminates TLS, validates the JWT, applies rate limiting, and routes the request to the correct backend service.
- Service discovery – a registry (Eureka, Consul) or platform DNS (Kubernetes) that resolves a logical service name to a healthy instance.
- Database-per-service – each service owns its data exclusively; no other service reads or writes its tables directly.
- Inter-service calls – synchronous (REST, gRPC) guarded by timeouts, retries, circuit breakers, and bulkheads; asynchronous (Kafka events) for decoupled flows.
- Correlation ID – a unique identifier propagated in headers across every hop so all log lines for one request can be gathered.
- Distributed tracing – each hop creates a span; parent and child spans are linked by trace context (W3C traceparent) and visualised as a waterfall in Jaeger or Zipkin.
Imagine Rajesh walks into National Bank of India to make a transfer. The security guard at the main door (API Gateway) checks his ID and points him to the correct counter. A directory (service discovery) tells the guard which counters are open. The transfer officer does the work using the transfer department's own files (database-per-service). To confirm details the officer phones the accounts department (inter-service call), with a timeout so he does not wait forever. The officer hands Rajesh a receipt (response), and every step is written in the branch logbook with the same reference number (correlation ID) so an auditor can follow the entire visit.
Understanding the Concepts
What happens at the API Gateway?
The client sends an HTTPS request to one public address. The gateway terminates TLS, validates the JWT or OAuth2 token, applies rate limiting, and routes the request to the correct backend by path.
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 GatewayConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder b) {
return b.routes()
.route(r -> r.path("/api/v1/transfers/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://transfer-service"))
.route(r -> r.path("/api/v1/accounts/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://account-service"))
.build();
}
}
How does service discovery work?
In a dynamic environment, service instances come and go. A registry like Eureka or Kubernetes DNS holds the current set of healthy instances. The gateway and inter-service clients resolve by logical name, never by hardcoded IP.
How does the service process the request?
The service receives the request in its controller, validates input, and calls its service layer, which executes business logic against its own database in a local transaction.
package com.bank.service;
import java.math.BigDecimal;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferService {
private final AccountRepository accountRepo;
private final TransferRepository transferRepo;
public TransferService(AccountRepository accountRepo, TransferRepository transferRepo) {
this.accountRepo = accountRepo;
this.transferRepo = transferRepo;
}
@Transactional
public Transfer execute(String fromId, String toId, BigDecimal amount) {
Account from = accountRepo.findById(fromId).orElseThrow();
Account to = accountRepo.findById(toId).orElseThrow();
from.debit(amount);
to.credit(amount);
accountRepo.save(from);
accountRepo.save(to);
return transferRepo.save(new Transfer(fromId, toId, amount));
}
}
How do inter-service calls work?
When a service needs data owned by another service, it makes a call guarded by resilience patterns.
Synchronous (REST/gRPC)
The transfer service calls the accounts service API and waits. Wrap with a timeout, retries, a circuit breaker, and a bulkhead.
@CircuitBreaker(name = "accountService", fallbackMethod = "fallback")
@Retry(name = "accountService")
@TimeLimiter(name = "accountService")
public Account fetchAccount(String accountId) {
return restClient.get()
.uri("http://account-service/api/accounts/{id}", accountId)
.retrieve()
.body(Account.class);
}
Asynchronous (Kafka events)
The service publishes an event and moves on. Consumers react independently.
kafkaTemplate.send("transfer.completed", transferId, event);
How does the response flow back?
The service builds a response, the controller serializes it, and it travels back through the gateway to the client. Non-essential follow-up (notifications, audit) continues asynchronously via events.
How do logging and tracing work?
When the request enters the gateway, it is tagged with a correlation ID. Every service propagates it in headers. All log lines for one request share that ID.
package com.bank.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
import java.util.UUID;
public class CorrelationIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
FilterChain chain) throws Exception {
String corrId = req.getHeader("X-Correlation-Id");
if (corrId == null) corrId = UUID.randomUUID().toString();
MDC.put("correlationId", corrId);
resp.setHeader("X-Correlation-Id", corrId);
try { chain.doFilter(req, resp); } finally { MDC.clear(); }
}
}
Expected log output:
[corrId=abc-123] TransferService: Debit SAV2001 50000.00
[corrId=abc-123] AccountClient: Calling account-service for SAV3002
[corrId=abc-123] TransferService: Transfer TXN-789 complete
Distributed tracing (Jaeger/Zipkin) shows the request as a waterfall of spans across services, so you can see exactly which hop was slow.
What are common mistakes?
// WRONG: hardcoded instance address
String url = "http://10.0.1.42:8080/api/accounts/SAV2001";
// CORRECT: logical service name resolved by discovery
String url = "http://account-service/api/accounts/SAV2001";
What should a senior engineer know?
- Keep services stateless so any instance behind discovery can serve any request.
- Propagate trace and correlation context on every hop; a missed hop creates a blind spot.
- Prefer events for anything that can be eventual to reduce coupling.
- Make retried operations idempotent so a retry after a timeout does not double-charge a customer.
- Design every synchronous dependency assuming it will one day be slow or down.
Visual Overview
Full Request Flow
┌────────┐ HTTPS ┌───────────┐ resolve ┌───────────┐
│ Client │ ──────► │ API │ ────────► │ Service │
│ (App) │ │ Gateway │ │ Discovery │
└────────┘ │ Auth+Rate │ └───────────┘
│ Limit │
└─────┬─────┘
│ route
▼
┌───────────┐ local tx ┌──────────┐
│ Transfer │ ──────────► │ DB │
│ Service │ │ (owned) │
└─────┬─────┘ └──────────┘
│ inter-service call
▼
┌───────────┐
│ Account │ (timeout + circuit breaker)
│ Service │
└───────────┘
Correlation ID: abc-123 propagated on every hop
┌────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐
│Gateway │─►│Transfer │─►│ Account │─►│ Kafka │
│span-1 │ │span-2 │ │ span-3 │ │ span-4 │
└────────┘ └─────────┘ └──────────┘ └─────────┘
Key Takeaway
The full flow of a microservices application is the path one request takes from the client through the system and back. The client sends HTTPS to the API Gateway, which authenticates, rate-limits, and routes to the correct service. Service discovery resolves the logical name to a healthy instance. The service runs its business logic against its own database in a local transaction, and where it needs another service's data it makes a guarded inter-service call with timeouts, retries, and a circuit breaker, or publishes an event for decoupled flows.
The response flows back through the gateway, and non-essential follow-up like notifications continues asynchronously via events. Cutting across every hop is a correlation ID propagated in headers so all log lines for one request can be gathered, and distributed tracing shows the request as a waterfall of spans so you can pinpoint exactly which call was slow or failed.
At National Bank of India, Pankaj built this exact flow for transfers. The gateway authenticated and routed, discovery resolved one of several transfer-service instances, the transfer service did the debit and credit in a local transaction, and a circuit breaker saved them when the accounts service was overloaded during a deployment by failing fast with a cached fallback. The single most valuable thing they built was disciplined correlation-ID propagation, because the first time a transfer failed intermittently they found the slow hop in Jaeger in minutes.
Remember one sentence: a request flows client to gateway to discovery to service to database, makes resilient inter-service calls guarded by timeouts and circuit breakers, returns through the gateway, and is stitched together end to end by a correlation ID in structured logs and distributed traces.
---