1. Your API works perfectly locally but becomes slow only in production. What would you check first?
Answer:Start with a latency breakdown. Check database time, downstream API time, queue time, thread pool saturation, GC pauses, CPU throttling, network latency, and gateway overhead. Local runs usually do not match production data size, traffic, TLS, rate limits, or dependency latency.
long start = System.nanoTime();
try {
return orderService.getOrder(id);
} finally {
long latencyMs = (System.nanoTime() - start) / 1_000_000;
log.info("operation=getOrder orderId={} latencyMs={}", id, latencyMs);
}
2. Kafka consumers are running normally, but message lag keeps increasing. Why can this happen?
Answer:The consumers may be alive but processing slower than messages are produced. Causes include slow handlers, too few partitions, hot partitions, DB/API slowness, rebalances, poison messages, GC pauses, or commits happening too late.
@KafkaListener(topics = "orders", groupId = "billing")
public void consume(OrderEvent event) throws InterruptedException {
process(event); // If this is slower than incoming rate, lag grows.
}
private void process(OrderEvent event) throws InterruptedException {
Thread.sleep(500); // Simulates slow DB or downstream call.
}
3. Database connections suddenly get exhausted during peak traffic. What could cause this?
Answer:Connection leaks, slow queries, too many request threads, missing timeouts, retry storms, long transactions, or a pool sized independently of database capacity can exhaust connections.
public User findUser(String id) throws SQLException {
String sql = "select id, name from users where id = ?";
try (Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setString(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
throw new NoSuchElementException("user not found");
}
return new User(rs.getString("id"), rs.getString("name"));
}
} // Connection is returned to the pool here.
}
4. Autoscaling creates more pods, but response time still keeps increasing.
Answer:Horizontal scaling does not fix a shared bottleneck. The real bottleneck may be the database, Redis, Kafka partitions, connection pools, a downstream API, locks, or a single hot key.
private final Semaphore dbLimit = new Semaphore(50);
public Order getOrder(String id) throws Exception {
if (!dbLimit.tryAcquire()) {
throw new TooManyRequestsException("database concurrency limit reached");
}
try {
return repository.findById(id).orElseThrow();
} finally {
dbLimit.release();
}
}
5. Retry logic starts creating duplicate payment transactions during failures.
Answer:Retries are dangerous for non-idempotent operations. Payment APIs must use idempotency keys so the same logical request returns the original result instead of charging again.
@Transactional
public Payment pay(String idempotencyKey, PaymentRequest request) {
return paymentRepository.findByIdempotencyKey(idempotencyKey)
.orElseGet(() -> {
Payment charged = paymentGateway.charge(request);
charged.setIdempotencyKey(idempotencyKey);
return paymentRepository.save(charged);
});
}
6. A scheduled job suddenly starts executing multiple times after scaling.
Answer:Each pod runs its own scheduler. After scaling from one pod to many pods, the same job can run once per pod unless you use a distributed lock, leader election, or an external scheduler.
@Scheduled(cron = "0 */5 * * * *")
@SchedulerLock(name = "invoiceJob", lockAtMostFor = "10m")
public void generateInvoices() {
invoiceService.run();
}
7. One slow downstream service starts affecting the entire platform.
Answer:Slow calls consume threads, connections, and queues. If dependencies share the same thread pool, one slow service can starve unrelated requests.
@TimeLimiter(name = "inventory")
@CircuitBreaker(name = "inventory", fallbackMethod = "fallback")
public CompletableFuture<Inventory> getInventory(String sku) {
return CompletableFuture.supplyAsync(
() -> inventoryClient.get(sku),
inventoryPool
);
}
private CompletableFuture<Inventory> fallback(String sku, Throwable ex) {
return CompletableFuture.completedFuture(Inventory.unavailable(sku));
}
8. APIs randomly return 500 errors, but infrastructure looks healthy.
Answer:Random 500s often come from application behavior: null values, race conditions, bad assumptions about input, serialization errors, timeout exceptions, pool exhaustion, or config differences.
@RestControllerAdvice
class ApiErrors {
@ExceptionHandler(Exception.class)
ResponseEntity<ErrorResponse> handle(Exception ex) {
log.error("request failed", ex);
return ResponseEntity.status(500)
.body(new ErrorResponse("internal_error"));
}
}
record ErrorResponse(String code) {}
9. Health checks pass, but users still face failures.
Answer:The health check may be too shallow. A process can be alive while unable to serve real traffic because the database, Kafka, cache, or required config is broken.
@GetMapping("/ready")
public ResponseEntity<Void> ready() {
boolean ready = databaseReachable() && kafkaReachable();
return ready
? ResponseEntity.ok().build()
: ResponseEntity.status(503).build();
}
@GetMapping("/live")
public ResponseEntity<Void> live() {
return ResponseEntity.ok().build();
}
10. Cache improves performance initially, but later starts returning stale data.
Answer:A cache needs an invalidation strategy. Stale data appears when writes update the database but not the cache, TTL is too long, or multiple services maintain inconsistent cache state.
@Cacheable(value = "users", key = "#id")
public User getUser(String id) {
return repository.findById(id).orElseThrow();
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
repository.save(user);
}
11. Logs exist everywhere, but debugging across services is still difficult.
Answer:Logs without correlation are hard to join. Add trace IDs, span IDs, structured logging, and distributed tracing so one request can be followed across services.
String traceId = Optional.ofNullable(request.getHeader("X-Trace-Id"))
.orElse(UUID.randomUUID().toString());
try {
MDC.put("traceId", traceId);
response.setHeader("X-Trace-Id", traceId);
chain.doFilter(request, response);
} finally {
MDC.clear();
}
12. JVM memory usage slowly increases after every deployment.
Answer:Look for memory leaks and deployment-related classloader leaks: static caches, unclosed clients, metrics labels with high cardinality, scheduled tasks, listeners, and ThreadLocal values.
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
public void handle(UserContext userContext) {
try {
CONTEXT.set(userContext);
processRequest();
} finally {
CONTEXT.remove(); // Important in reused server threads.
}
}
13. APIs work in staging but fail behind the production gateway.
Answer:Production gateway behavior can differ: path rewriting, auth headers, TLS, CORS, body size limits, timeouts, rate limits, forwarded headers, or blocked methods.
@GetMapping("/orders/{id}")
public Order getOrder(
@PathVariable String id,
@RequestHeader(value = "X-Forwarded-Proto", required = false) String proto,
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
log.info("requestId={} forwardedProto={}", requestId, proto);
return service.get(id);
}
14. Thread pools become exhausted even though CPU usage is stable.
Answer:Low CPU with exhausted threads usually means threads are blocked, not computing. They may be waiting on DB connections, slow HTTP calls, locks, queues, file I/O, or sleeps.
ThreadPoolExecutor pool = new ThreadPoolExecutor(
20,
20,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.AbortPolicy()
);
// Bounded queue prevents unlimited request buildup.
15. Circuit breakers are configured, but cascading failures still happen.
Answer:Circuit breakers are only one guardrail. Cascading failures still happen when retries are too aggressive, timeouts are too long, dependencies share thread pools, fallbacks are expensive, or there is no backpressure.
@Retry(name = "payment")
@CircuitBreaker(name = "payment", fallbackMethod = "fallback")
@Bulkhead(name = "payment", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<PaymentResult> charge(PaymentRequest request) {
return CompletableFuture.supplyAsync(() -> paymentClient.charge(request));
}
private CompletableFuture<PaymentResult> fallback(PaymentRequest request, Throwable ex) {
return CompletableFuture.completedFuture(PaymentResult.pending());
}