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.
| Type | Mechanism | Recommended? | Why / When |
|---|---|---|---|
| Constructor Injection | @Autowired on constructor (implicit in Spring 4.3+) | ✓ Preferred | Immutable, testable, no NPE, prevents circular deps at startup |
| Setter Injection | @Autowired on setter method | Optional deps only | For optional/overrideable dependencies |
| Field Injection | @Autowired on field directly | ✗ Avoid | Hides dependencies, breaks unit testing without Spring context, hard to make final |
Why is constructor injection preferred over field injection? What problem does it solve?
final — guarantees they're always setnew MyService(mockRepo) — no Spring context neededField injection looks convenient but creates hidden dependencies and makes the class harder to test and maintain. I enforce constructor injection in code reviews.
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.
// ❌ 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); } }
| Scope | One instance per… | Thread Safe? | Production Use |
|---|---|---|---|
| singleton (default) | ApplicationContext | No — stateful fields are dangerous | Stateless services, repositories, DAOs |
| prototype | Each injection/getBean() call | Yes | Stateful command objects, non-thread-safe helpers |
| request | HTTP request | Yes | Request-scoped data (user context, correlation ID) |
| session | HTTP session | Yes | User session preferences |
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.
How do you inject a prototype-scoped bean into a singleton bean?
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:
provider.get() on demand| Proxy Type | How | Requires | Spring Default |
|---|---|---|---|
| JDK Dynamic Proxy | Creates proxy implementing the same interfaces | Bean must implement an interface | When interface exists (Spring 4 and earlier default) |
| CGLIB Proxy | Subclasses the bean class at runtime (bytecode generation) | Class must not be final. Methods not final. | Spring Boot default (proxyTargetClass=true) |
Why does @Transactional fail in self-invocation? How do you fix it?
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:
@Autowired private PaymentService self; then call self.methodB() — goes through proxymethodB() to a separate PaymentWriter bean — cleanest solution@EnableTransactionManagement(mode = AspectJ)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.
@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); } }
| Term | Meaning | Example |
|---|---|---|
| Aspect | Modular cross-cutting concern | LoggingAspect, TransactionAspect |
| Advice | Code to run at join point | @Before, @After, @Around, @AfterReturning, @AfterThrowing |
| Pointcut | Predicate matching join points | execution(* com.wf.payment.*.*(..)) |
| Join Point | Specific moment during execution | Method call, exception throw |
| Weaving | Applying aspects to targets | Compile-time (AspectJ), load-time, runtime (proxy) |
How does Spring Boot auto-configure DataSource? Walk me through the full flow.
DataSourceAutoConfiguration is listed in AutoConfiguration.imports@ConditionalOnClass(DataSource.class, EmbeddedDatabaseType.class) — only runs if a JDBC driver is on classpath@ConditionalOnMissingBean(DataSource.class) — only creates DataSource if you haven't defined your ownspring.datasource.* properties → creates HikariCP DataSource by default@Bean DataSource — the auto-config backs off completelyThis "backing-off" pattern is the genius of Spring Boot — sensible defaults, full override capability.
// 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 }
| Propagation | Behaviour | Use Case |
|---|---|---|
| REQUIRED (default) | Join existing tx, or create new if none | Most service methods |
| REQUIRES_NEW | Always creates NEW transaction. Suspends existing. | Audit logging — must commit even if main tx rolls back |
| SUPPORTS | Join tx if exists, run without if not | Read-only query methods |
| NOT_SUPPORTED | Always run without transaction. Suspends existing. | Bulk export — doesn't need tx overhead |
| NEVER | Throws exception if transaction exists | Prevent accidental tx wrapping |
| NESTED | Savepoint inside existing tx — partial rollback | Line items in a payment batch |
In a payment system, you need audit logging to persist even when the payment transaction fails. How do you do this?
Use REQUIRES_NEW propagation on the audit method:
@Transactional(propagation = REQUIRES_NEW)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.
@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!
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.
| Dimension | Monolith | Microservices |
|---|---|---|
| Deployment | One artefact — simple | Independent per service |
| Scalability | Scale whole app | Scale only hot services |
| Data consistency | ACID transactions trivial | Eventual consistency — complex |
| Network | In-process calls — fast | Network latency + failures |
| Team scaling | Coordination overhead | Teams own services independently |
| Debugging | Single process — easy | Distributed tracing required |
| Complexity | Low operational overhead | Service mesh, discovery, circuit breakers |
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.
// 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 }
How do you prevent a slow fraud detection service from causing all payment threads to block?
| Approach | How | Problem | Use In Microservices? |
|---|---|---|---|
| 2PC (Two-Phase Commit) | Coordinator asks all participants to prepare, then commit | Tight 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 failures | Central orchestrator could become bottleneck | Preferred for payments |
If Step 3 fails → run compensating transactions for Steps 2 and 1 in reverse order
| Saga Type | How Communication Works | Pros | Cons |
|---|---|---|---|
| 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. |
@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! } }
At Wells Fargo running on Kubernetes, do you still need Eureka or Consul?
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.
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.
What is the difference between BeanFactory and ApplicationContext?
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.
What is a circular dependency in Spring? How do you resolve it?
Difference between @Component, @Service, @Repository, @Controller?
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.
What does @SpringBootApplication do internally?
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.
How do you handle exceptions globally in a Spring Boot REST API?
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.
What is an API Gateway? What should it and should NOT do?
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.
How do you maintain API backward compatibility in microservices?
What is the Strangler Fig pattern? When would you use it?
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.
How do you trace a request across 5 microservices in production?
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.
What is CQRS? When would you use it in a payment system?
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.
What is idempotency? How do you implement it in a payment API?
X-Idempotency-Key: <uuid> headerExplain eventual consistency. How do you handle it in a payment balance display?
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".