Wells Fargo Lead Java Engineer · Week 2 of 4

Spring Boot
+ Microservices
Architecture

The heart of the role. Go beyond "I know Spring Boot" — speak like an architect who has debugged proxy failures at 2am in a payment outage.

🌿 Heart of the Role Spring Internals IoC Container AOP + Proxy Auto-Config Microservices Saga Pattern Circuit Breaker Service Discovery
01
Chapter One
IoC Container & Dependency Injection
The foundation of every Spring app. Understand the container as an object factory + lifecycle manager, not just "annotations that wire stuff".
🌿
Inversion of Control — The Big Picture
BeanFactory · ApplicationContext · DI types
Foundation
IoC Container — What It Actually Does
Spring IoC Container — Object Factory + Lifecycle Manager
Configuration Sources
@Configuration classes
@Component scanning
XML / Properties
@Bean methods
IoC Container Manages
PaymentService (singleton)
AccountRepository (singleton)
KafkaProducer (singleton)
DataSource (singleton)
Container handles: instantiation → dependency injection → lifecycle callbacks → destruction
DI Types — Know the Difference
TypeMechanismRecommended?Why / When
Constructor Injection@Autowired on constructor (implicit in Spring 4.3+)✓ PreferredImmutable, testable, no NPE, prevents circular deps at startup
Setter Injection@Autowired on setter methodOptional deps onlyFor optional/overrideable dependencies
Field Injection@Autowired on field directly✗ AvoidHides dependencies, breaks unit testing without Spring context, hard to make final
🎤 Interview Question

Why is constructor injection preferred over field injection? What problem does it solve?

✅ Lead-Level Answer
  • Immutability: fields can be final — guarantees they're always set
  • Testability: unit tests can instantiate with new MyService(mockRepo) — no Spring context needed
  • Fail-fast: circular dependencies detected at startup, not at runtime
  • Explicit contract: class dependencies are visible in constructor signature — self-documenting

Field injection looks convenient but creates hidden dependencies and makes the class harder to test and maintain. I enforce constructor injection in code reviews.

⚠️ Common Mistake

Candidates often say "field injection is fine for small apps". In production banking systems, testability is non-negotiable. Always defend constructor injection with the testability argument.

InjectionStyles.java
// ❌ Field injection — avoid in production
@Service
public class PaymentService {
    @Autowired private AccountRepository repo; // hidden dep
    @Autowired private FraudService fraudService;
}

// ✅ Constructor injection — always preferred
@Service
public class PaymentService {
    private final AccountRepository repo;
    private final FraudService fraudService;

    // @Autowired implicit if single constructor (Spring 4.3+)
    public PaymentService(AccountRepository repo, FraudService fraudService) {
        this.repo = repo;
        this.fraudService = fraudService;
    }
}

// ✅ Unit test — no Spring context needed
class PaymentServiceTest {
    @Test
    void test() {
        var service = new PaymentService(mockRepo, mockFraud); // clean!
        service.process(payment);
    }
}
02
Chapter Two
Bean Lifecycle & Scopes
Interviewers love lifecycle questions. Know the full sequence and why each phase matters in a payment application.
🔄
Full Bean Lifecycle — Every Step
Instantiation → Injection → Aware → PostConstruct → Ready → PreDestroy
Critical
Spring Bean Lifecycle — Complete Sequence
1
Instantiation
Constructor called. Object created in heap.
2
Dependency Injection
@Autowired fields/setters populated by container.
3
BeanNameAware / ApplicationContextAware
Container sets bean name, injects ApplicationContext if needed.
4
BeanPostProcessor (Before)
postProcessBeforeInitialization — AOP proxy creation starts here.
5
@PostConstruct / InitializingBean.afterPropertiesSet()
Your custom init logic. DB connection pool warm-up, cache pre-load, config validation.
6
BeanPostProcessor (After)
postProcessAfterInitialization — AOP proxy wraps bean here. What you inject is the proxy, not the original bean.
7
Bean Ready — In Use
Bean is fully initialised and available for injection/use throughout application lifetime.
8
@PreDestroy / DisposableBean.destroy()
Cleanup: close DB connections, flush caches, cancel scheduled tasks, shutdown Kafka consumers.
Bean Scopes
ScopeOne instance per…Thread Safe?Production Use
singleton (default)ApplicationContextNo — stateful fields are dangerousStateless services, repositories, DAOs
prototypeEach injection/getBean() callYesStateful command objects, non-thread-safe helpers
requestHTTP requestYesRequest-scoped data (user context, correlation ID)
sessionHTTP sessionYesUser session preferences
💡 Production Insight

Singleton beans with mutable instance fields are a race condition. If a singleton PaymentService has a field private Payment currentPayment, two simultaneous requests will corrupt each other. Use ThreadLocal or pass state as method parameters.

🎤 Interview Question

How do you inject a prototype-scoped bean into a singleton bean?

✅ Answer — This Is a Trap Question

Simply @Autowireding a prototype into a singleton doesn't work — the singleton holds the prototype reference injected at startup. It's effectively a singleton.

Solutions:

  • @Lookup method injection — Spring overrides the method to return a new prototype each time
  • ApplicationContext.getBean() — inject the context and call getBean() each time
  • ObjectFactory / Provider<T> — Spring injects a factory, call provider.get() on demand
03
Chapter Three
AOP & Spring Proxy Mechanism
The most misunderstood Spring concept. AOP is the reason @Transactional, @Cacheable, and @Async can silently stop working — and why every lead engineer must understand proxies.
🔮
AOP + Proxy Deep Dive
JDK Proxy · CGLIB · @Transactional self-invocation bug
Most Tricky
How Spring AOP Works — Proxy Pattern
Spring AOP — Proxy Wraps Your Bean
✅ External Call (Works)
Caller injects PaymentService
Gets PROXY not real bean
Proxy intercepts call
Begins @Transactional ✓
Delegates to real bean
❌ Self-Invocation (Fails)
methodA() calls this.methodB()
Uses this reference
Bypasses proxy entirely
@Transactional IGNORED ✗
No transaction started!
JDK Proxy vs CGLIB
Proxy TypeHowRequiresSpring Default
JDK Dynamic ProxyCreates proxy implementing the same interfacesBean must implement an interfaceWhen interface exists (Spring 4 and earlier default)
CGLIB ProxySubclasses the bean class at runtime (bytecode generation)Class must not be final. Methods not final.Spring Boot default (proxyTargetClass=true)
🔥 Most Asked Spring Question at Lead Level

Why does @Transactional fail in self-invocation? How do you fix it?

✅ Expert Answer

Spring wraps your bean in a proxy at BeanPostProcessor time. When another bean injects PaymentService, they get the proxy. The proxy adds transaction logic before delegating to the real bean.

Self-invocation uses this — the real bean reference, not the proxy. The call completely bypasses the proxy → no transaction begins.

Three fixes:

  • Inject self: @Autowired private PaymentService self; then call self.methodB() — goes through proxy
  • Extract to a new bean: move methodB() to a separate PaymentWriter bean — cleanest solution
  • AspectJ mode: compile-time or load-time weaving bypasses proxy entirely — wires advice into bytecode directly. @EnableTransactionManagement(mode = AspectJ)
⚠️ Silent Bug in Payments

This is a very common production data-consistency bug. A payment method calls a refund method within the same class thinking they're in the same transaction. They're not — the refund has no transaction. Data is half-written. This type of bug causes reconciliation failures in banking.

TransactionalSelfInvocation.java
@Service
public class PaymentService {

    @Autowired
    private PaymentService self; // injects the PROXY

    // ❌ BAD — self-invocation bypasses proxy
    public void processAndNotify(Payment p) {
        this.savePayment(p);   // NO transaction! this = real bean
        sendNotification(p);
    }

    // ✅ GOOD — calls proxy, transaction starts
    public void processAndNotify(Payment p) {
        self.savePayment(p);   // goes through proxy ✓
        sendNotification(p);
    }

    @Transactional
    public void savePayment(Payment p) {
        repo.save(p);
        auditRepo.save(new AuditLog(p));
    }
}

// ✅ BEST — extract to separate bean (cleanest architecture)
@Service
public class PaymentWriter {
    @Transactional
    public void save(Payment p) { repo.save(p); }
}
AOP Terminology — Know These Cold
TermMeaningExample
AspectModular cross-cutting concernLoggingAspect, TransactionAspect
AdviceCode to run at join point@Before, @After, @Around, @AfterReturning, @AfterThrowing
PointcutPredicate matching join pointsexecution(* com.wf.payment.*.*(..))
Join PointSpecific moment during executionMethod call, exception throw
WeavingApplying aspects to targetsCompile-time (AspectJ), load-time, runtime (proxy)
04
Chapter Four
Spring Boot Auto-Configuration
The magic behind @SpringBootApplication. Understanding the internals lets you debug missing beans, override configurations, and write your own starters.
⚙️
Auto-Configuration Internals
@Conditional · AutoConfiguration.imports · Starter anatomy
Critical
Auto-Configuration Resolution Flow
@SpringBootApplication
↓ includes
@EnableAutoConfiguration
↓ triggers
AutoConfigurationImportSelector
↓ reads
META-INF/spring/AutoConfiguration.imports (Boot 3)
META-INF/spring.factories (Boot 2)
↓ loads 100s of AutoConfig classes, each checks
@ConditionalOnClass
@ConditionalOnMissingBean
@ConditionalOnProperty
@ConditionalOnWebApplication
↓ conditions pass →
Beans registered in context ✓
🎤 Interview Question

How does Spring Boot auto-configure DataSource? Walk me through the full flow.

✅ Step-by-Step Answer
  • DataSourceAutoConfiguration is listed in AutoConfiguration.imports
  • Has @ConditionalOnClass(DataSource.class, EmbeddedDatabaseType.class) — only runs if a JDBC driver is on classpath
  • Has @ConditionalOnMissingBean(DataSource.class) — only creates DataSource if you haven't defined your own
  • Reads spring.datasource.* properties → creates HikariCP DataSource by default
  • If you define your own @Bean DataSource — the auto-config backs off completely

This "backing-off" pattern is the genius of Spring Boot — sensible defaults, full override capability.

CustomAutoConfiguration.java — Write Your Own Starter
// Custom auto-configuration for payment audit starter
@Configuration
@ConditionalOnClass(PaymentAuditService.class)
@ConditionalOnProperty(prefix = "payment.audit", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(PaymentAuditProperties.class)
public class PaymentAuditAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean  // back off if user provides their own
    public PaymentAuditService paymentAuditService(PaymentAuditProperties props) {
        return new PaymentAuditService(props.getRetentionDays());
    }
}

// Register in: META-INF/spring/AutoConfiguration.imports
// com.wellsfargo.payment.audit.PaymentAuditAutoConfiguration

// Properties class
@ConfigurationProperties(prefix = "payment.audit")
public class PaymentAuditProperties {
    private int retentionDays = 90; // default
    // getter/setter
}
Profiles + Externalized Configuration
DEV
H2 in-memory DB
Debug logging
Actuator all endpoints
No SSL
UAT
Oracle UAT schema
Info logging
Limited Actuator
SSL enabled
PROD
Oracle RAC cluster
Warn/Error only
Health endpoint only
mTLS + Vault secrets
Config Priority
1. CLI args
2. Env vars
3. Config Server
4. application-{profile}.yml
5. application.yml
05
Chapter Five
@Transactional — Full Internals
One of the most misused annotations in enterprise Java. Know propagation, isolation, rollback rules, and the self-invocation gotcha by heart.
💰
@Transactional Deep Dive
Propagation · Isolation · Rollback · Common bugs
Critical for Payments
Propagation Levels — Most Common
PropagationBehaviourUse Case
REQUIRED (default)Join existing tx, or create new if noneMost service methods
REQUIRES_NEWAlways creates NEW transaction. Suspends existing.Audit logging — must commit even if main tx rolls back
SUPPORTSJoin tx if exists, run without if notRead-only query methods
NOT_SUPPORTEDAlways run without transaction. Suspends existing.Bulk export — doesn't need tx overhead
NEVERThrows exception if transaction existsPrevent accidental tx wrapping
NESTEDSavepoint inside existing tx — partial rollbackLine items in a payment batch
🎤 Interview Question

In a payment system, you need audit logging to persist even when the payment transaction fails. How do you do this?

✅ Production Answer

Use REQUIRES_NEW propagation on the audit method:

  • Main payment tx → calls auditService.log()
  • auditService.log() has @Transactional(propagation = REQUIRES_NEW)
  • This suspends the payment tx, opens a NEW transaction just for the audit
  • Audit tx commits independently. Then main tx resumes and may roll back.
  • Audit record persists regardless of payment outcome
💡 Bonus Pattern

For audit logs in high-volume systems, consider publishing an event (Kafka/Spring Events) instead. The audit service consumes it independently — even more decoupled and resilient.

TransactionPropagation.java
@Service
public class PaymentService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void processPayment(Payment p) {
        accountRepo.debit(p.getAmount());
        auditService.log(p);  // REQUIRES_NEW — commits separately!
        if (fraudDetected) throw new FraudException();  // rolls back debit
        // audit log still persisted ✓
    }
}

@Service
public class AuditService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(Payment p) {
        // Opens new transaction, commits independently
        auditRepo.save(new AuditRecord(p, LocalDateTime.now()));
    }
}

// Rollback rules — important!
@Transactional(
    rollbackFor = {PaymentException.class, IOException.class},
    noRollbackFor = {ValidationException.class}
)
// Default: rolls back on RuntimeException + Error
// Does NOT roll back checked exceptions by default!
⚠️ Default Rollback Trap

By default, @Transactional only rolls back on RuntimeException and Error. Checked exceptions do NOT trigger rollback unless you specify rollbackFor. In a payment service that throws a checked PaymentException, forgetting this = partial DB write without rollback.

06
Chapter Six
Microservices Architecture
Think like an architect. Know the tradeoffs, failure modes, and patterns — not just "services talk over HTTP".
🏛️
Microservices vs Monolith + Core Patterns
Tradeoffs · API Gateway · Service Mesh · Communication
Architect Level
Wells Fargo Payments Platform — Architecture
Microservices Architecture — Payment Platform
External Clients
Mobile App
Web Banking
3rd Party API
↓ ↓ ↓
Edge Layer
API Gateway
Auth · Rate Limit · Routing · SSL Termination
↓ ↓ ↓
Business Services
Payment
Service
Account
Service
Fraud
Detection
Notification
Service
↓ event bus ↓
Async Messaging
Kafka / Event Bus
Decoupled async communication
↓ ↓ ↓
Data Layer
Oracle
(Payments)
Oracle
(Accounts)
Redis
(Cache)
MongoDB
(Audit)
Monolith vs Microservices — Tradeoffs
DimensionMonolithMicroservices
DeploymentOne artefact — simpleIndependent per service
ScalabilityScale whole appScale only hot services
Data consistencyACID transactions trivialEventual consistency — complex
NetworkIn-process calls — fastNetwork latency + failures
Team scalingCoordination overheadTeams own services independently
DebuggingSingle process — easyDistributed tracing required
ComplexityLow operational overheadService mesh, discovery, circuit breakers
💡 Architect's Answer for When to Choose

Start with a modular monolith. Extract services when you have team scaling needs or independent scaling requirements — not just because "microservices is modern". Premature decomposition creates distributed monolith: worst of both worlds.

07
Chapter Seven
Resilience Patterns
Circuit Breaker, Retry, Bulkhead, Timeout, Rate Limiter — the patterns that keep payment services alive when dependencies fail.
🛡️
Circuit Breaker + Resilience4j Patterns
CB states · Bulkhead · Retry · Rate Limiter · Timeout
Critical
Circuit Breaker — State Machine (Resilience4j)
🟢
CLOSED
Normal. Requests flow through. Tracking failure rate in sliding window.
failure rate ≥ threshold success!
🔴
OPEN
FAST FAIL. All requests immediately rejected. No calls to downstream.
wait duration expires
🟡
HALF-OPEN
Test probe: allow N calls. Success → CLOSED. Failure → OPEN again.
Circuit Breaker
Fail fast on cascade
  • Prevent cascading failures
  • 3 states: CLOSED/OPEN/HALF
  • Sliding window metrics
  • Fallback responses
🔁
Retry
Transient fault tolerance
  • Exponential backoff
  • Max attempt limit
  • Jitter to spread retries
  • Only for idempotent ops
🧱
Bulkhead
Isolate failure domains
  • Separate thread pools per service
  • Semaphore for concurrency limit
  • Slow payment API can't starve fraud API
  • Prevents resource exhaustion
⏱️
Timeout
Don't wait forever
  • Time-limit all remote calls
  • Release threads promptly
  • Return fallback on timeout
  • Set per-downstream service
🚦
Rate Limiter
Control request flow
  • Protect downstream from overload
  • Token bucket algorithm
  • Per-client limits in API Gateway
  • 429 Too Many Requests response
Resilience4jConfig.java
// Resilience4j Circuit Breaker config
CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)          // open at 50% failure rate
    .waitDurationInOpenState(Duration.ofSeconds(30))
    .slidingWindowSize(20)              // last 20 calls
    .permittedNumberOfCallsInHalfOpenState(5)
    .recordExceptions(IOException.class, TimeoutException.class)
    .build();

// Usage with fallback
@CircuitBreaker(name = "fraudService", fallbackMethod = "fraudFallback")
@Retry(name = "fraudService")
@Bulkhead(name = "fraudService", type = Bulkhead.Type.THREADPOOL)
public FraudResult checkFraud(Payment payment) {
    return fraudClient.check(payment);
}

public FraudResult fraudFallback(Payment payment, Exception e) {
    log.warn("Fraud service unavailable, using default policy");
    return FraudResult.ALLOW_WITH_ENHANCED_MONITORING; // degrade gracefully
}
🎤 Interview Question

How do you prevent a slow fraud detection service from causing all payment threads to block?

✅ Answer — Three Layers of Defence
  • Timeout: set 2-second timeout on fraud API call — threads released promptly
  • Bulkhead: isolate fraud calls in separate thread pool (10 threads). Fraud slowness can't consume the main payment thread pool
  • Circuit Breaker: if fraud service fails consistently, open circuit — fast-fail for 30s, reduce pressure, use fallback policy
  • Fallback: allow payment with enhanced monitoring instead of rejecting — don't let fraud service become payment service's single point of failure
08
Chapter Eight
Saga Pattern — Distributed Transactions
The pattern that replaces 2PC in microservices. Every payment architect needs to know choreography vs orchestration by heart.
📜
Saga Pattern — Choreography & Orchestration
Compensating transactions · Forward steps · Failure handling
Must Know
Why Not 2PC?
ApproachHowProblemUse In Microservices?
2PC (Two-Phase Commit)Coordinator asks all participants to prepare, then commitTight coupling. Coordinator = SPOF. Blocking. All services must support XA.Avoid
Saga (Choreography)Each service publishes events. Others react.Hard to track. Cyclic dependencies risk.Good for simple flows
Saga (Orchestration)Central orchestrator directs each step and handles failuresCentral orchestrator could become bottleneckPreferred for payments
Payment Saga — Forward + Compensating Steps
Orchestrated Saga — Wire Transfer
Forward Step
Compensating Step (Rollback)
1
Debit Source Account
Credit Source Account (Reverse)
2
Reserve Funds
Release Reserved Funds
3
SWIFT Message to Beneficiary Bank
Recall SWIFT Message
4
Credit Beneficiary Account
Debit Beneficiary (if possible)
5
Update Payment Status → SETTLED
Update Status → FAILED/REVERSED

If Step 3 fails → run compensating transactions for Steps 2 and 1 in reverse order

Saga TypeHow Communication WorksProsCons
Choreography Services publish events. Others subscribe and react. No central brain. No SPOF. Loose coupling. Simple to start. Hard to trace end-to-end. Risk of cyclic event loops. Difficult to debug.
Orchestration Saga Orchestrator (Temporal, Axon, or custom) issues commands. Services report back. Clear flow visibility. Centralised error handling. Easier debugging. Orchestrator can be a bottleneck. More coupling to orchestrator.
PaymentSagaOrchestrator.java (Axon Framework)
@Saga
public class PaymentSaga {

    @Autowired private transient CommandGateway commandGateway;

    @StartSaga
    @SagaEventHandler(associationProperty = "paymentId")
    public void on(PaymentInitiatedEvent event) {
        // Step 1: Debit source account
        commandGateway.send(new DebitAccountCommand(
            event.getFromAccount(), event.getAmount(), event.getPaymentId()
        ));
    }

    @SagaEventHandler(associationProperty = "paymentId")
    public void on(AccountDebitedEvent event) {
        // Step 2: Credit beneficiary
        commandGateway.send(new CreditAccountCommand(
            event.getToAccount(), event.getAmount(), event.getPaymentId()
        ));
    }

    // Compensating transaction — triggered on failure
    @SagaEventHandler(associationProperty = "paymentId")
    public void on(PaymentFailedEvent event) {
        // Reverse debit — run compensating transactions in reverse order
        commandGateway.send(new ReverseDebitCommand(
            event.getFromAccount(), event.getAmount()
        ));
        SagaLifecycle.end();
    }

    @EndSaga
    @SagaEventHandler(associationProperty = "paymentId")
    public void on(AccountCreditedEvent event) {
        // Payment complete!
    }
}
09
Chapter Nine
Service Discovery & Config Server
How services find each other and how configuration is managed across hundreds of microservice instances.
🔭
Service Discovery + Config Management
Eureka · Consul · K8s DNS · Spring Cloud Config · Vault
Important
Service Discovery — How Services Find Each Other
Service Registry Pattern
Registered Services
payment-svc:8081
payment-svc:8082
account-svc:8090
fraud-svc:9001 ✗
Service Registry
Eureka / Consul / K8s DNS
Heartbeat every 30s
De-register on failure
Consumers
notification-svc
audit-svc
api-gateway
🎤 Interview Question

At Wells Fargo running on Kubernetes, do you still need Eureka or Consul?

✅ Architect Answer

In Kubernetes, service discovery is built-in via Kubernetes DNS + Services (ClusterIP). Every Service gets a stable DNS name: payment-svc.default.svc.cluster.local. No Eureka needed.

Use Eureka/Consul when: running on VMs without Kubernetes, multi-datacenter cross-cluster discovery, or hybrid cloud with non-K8s services.

At Wells Fargo on OCP (OpenShift Container Platform): OpenShift Routes + Services handle discovery. Spring Cloud LoadBalancer client-side LB is still useful for fine-grained routing.

Spring Cloud Config Server Flow
Git Repo
application.yml per service
Config Server
Spring Cloud Config
Microservice
fetches on startup
Bus Refresh
@RefreshScope
💡 Security Insight for Banking

Never put secrets (DB passwords, API keys) in Git-backed Config Server — even encrypted. Use HashiCorp Vault integration. Spring Cloud Vault pulls secrets at startup from Vault. Secrets are never in version control, are rotated automatically, and access is audited. This is standard practice at financial institutions.

Q
Practice Round
Week 2 — Full Question Bank
All high-probability Spring Boot + Microservices questions for a Lead/Architect-level interview. Practice saying these out loud.
🎤
35+ Questions with Answer Skeletons
Spring · Microservices · Saga · Resilience · Architecture tradeoffs
Practice These
Spring Framework
Q1

What is the difference between BeanFactory and ApplicationContext?

Answer

BeanFactory is the basic IoC container — lazy init, no AOP, no event publishing. ApplicationContext extends it: eager singleton init, AOP integration, MessageSource (i18n), ApplicationEventPublisher, resource loading. Always use ApplicationContext in production. BeanFactory only for memory-constrained environments.

Q2

What is a circular dependency in Spring? How do you resolve it?

Answer
  • A → B → A (A depends on B which depends on A)
  • With constructor injection: Spring throws BeanCurrentlyInCreationException at startup (good — fail fast)
  • With field injection: Spring resolves it via 3-level cache (singletonFactories) but leaves partially initialised beans
  • Fix: refactor to break the cycle. Extract common dependency. Use @Lazy on one injection. Or use ApplicationEventPublisher to decouple
Q3

Difference between @Component, @Service, @Repository, @Controller?

Answer

All are specialisations of @Component — functionally identical for component scanning. Semantic differences: @Repository enables Spring's PersistenceExceptionTranslation (wraps JDBC/JPA exceptions to DataAccessException hierarchy). @Service and @Controller are purely semantic — convey architectural layer intent, improve readability and AOP targeting.

Q4

What does @SpringBootApplication do internally?

Answer

It's a convenience annotation combining: @Configuration (this class defines beans), @EnableAutoConfiguration (triggers auto-config loading), @ComponentScan (scans package and sub-packages for @Component classes). Equivalent to using all three individually.

Q5

How do you handle exceptions globally in a Spring Boot REST API?

Answer

Use @ControllerAdvice + @ExceptionHandler. Create a GlobalExceptionHandler class annotated @RestControllerAdvice. Methods annotated @ExceptionHandler(SomeException.class) intercept that exception globally and return a structured error response (ErrorResponse DTO). Benefits: centralised, no try-catch in controllers, consistent error format for API consumers.

Microservices
Q6

What is an API Gateway? What should it and should NOT do?

Answer

Should do: SSL termination, authentication/JWT validation, rate limiting, routing, request logging, correlation ID injection, response caching, load balancing.

Should NOT do: business logic, data transformation beyond simple header manipulation, database calls. Keep it thin — it's a traffic cop, not a service.

Q7

How do you maintain API backward compatibility in microservices?

Answer
  • Never remove or rename fields from response — add only
  • URL versioning: /api/v1/payments, /api/v2/payments
  • Header versioning: Accept: application/vnd.wf.payment.v2+json
  • Consumer-driven contract testing (Pact) — verify consumers before changing API
  • Keep old version alive during migration period with deprecation headers
Q8

What is the Strangler Fig pattern? When would you use it?

Answer

Gradually migrate a monolith to microservices by routing some traffic to new services while the monolith handles the rest. New functionality built as microservices. Traffic "strangles" the monolith over time. Safe incremental migration — no big-bang rewrite. Used when migrating a legacy banking system to microservices at Wells Fargo.

Q9

How do you trace a request across 5 microservices in production?

Answer

Distributed tracing with correlation IDs. Spring Cloud Sleuth (or Micrometer Tracing in Boot 3) auto-generates trace ID + span ID. Inject into MDC → appears in all logs. Propagated via HTTP headers (B3 or W3C TraceContext format). Visualised in Zipkin or Jaeger. Query by trace ID to see full request journey across all 5 services with timing per service.

Q10

What is CQRS? When would you use it in a payment system?

Answer

Command Query Responsibility Segregation — separate the write model (commands: initiate payment) from the read model (queries: payment history, balance). Write side optimised for consistency. Read side optimised for query performance (denormalised, cached, replicated). Use when read and write patterns have very different requirements — e.g. payment initiation needs ACID, payment reporting needs fast flexible queries across millions of records.

Transactions + Resilience
Q11

What is idempotency? How do you implement it in a payment API?

Answer
  • Idempotency: same request can be sent multiple times without different outcome
  • Client sends unique X-Idempotency-Key: <uuid> header
  • Server stores key + response in Redis with TTL 24h
  • On duplicate request: return cached response, skip processing
  • Use distributed lock (Redis SETNX) to prevent race if two identical requests arrive simultaneously
  • DB unique constraint on idempotency key as final safety net
Q12

Explain eventual consistency. How do you handle it in a payment balance display?

Answer

After a write, not all replicas immediately have the same data — they converge over time. For balance display: show "pending" status for transactions in-flight. Use read-after-write consistency for the user's own actions (route their reads to primary DB briefly after write). For non-critical reads use replica. For critical balance (transfer confirmation), always read from primary. Be transparent — "balance updates may take a few minutes to reflect".