Definition
- @Scheduled -- runs a method on a timer;
fixedRatefires at a constant interval (even if the previous run is still going),fixedDelaywaits after the previous run completes, andcronuses a calendar expression like0 0 2 * * *for 2 AM daily. - @Retryable -- from spring-retry, retries a method on exception with configurable max attempts and exponential backoff;
@Recoverprovides a fallback when retries are exhausted. - Flyway -- a database migration tool that runs versioned SQL scripts (V1__create_tables.sql) on startup and tracks applied migrations in a
flyway_schema_historytable. - Testcontainers -- a library that starts real Docker containers (PostgreSQL, Redis, Kafka) for integration tests, ensuring tests use the same database engine as production.
- @Value -- injects a single property (
@Value("${db.url}")) or evaluates a SpEL expression (@Value("#{T(java.time.LocalDate).now()}"). - CommandLineRunner -- a functional interface that runs once after the application context is fully initialized; ideal for bootstrap tasks like loading reference data.
@Scheduled is the clock at National Bank of India that triggers the end-of-day interest calculation at 3 PM every weekday. @Retryable is the teller who redials a busy core banking phone line with increasing wait times -- 1 second, 2 seconds, 4 seconds -- before giving up and sending Priya to the manual queue. Flyway is the building renovation log that records every structural change so no renovation is repeated. Testcontainers is a temporary mini bank vault set up just for testing and torn down afterward.
Understanding the Concepts
What is @Scheduled and how do fixedRate, fixedDelay, and cron differ?
@Scheduled runs a method on a timer. You must annotate a @Configuration class with @EnableScheduling to activate it. There are three scheduling modes, and understanding the difference is critical for avoiding overlapping executions.
- fixedRate fires at a constant interval regardless of how long the previous execution takes. If the method takes 10 seconds and fixedRate is 5 seconds, the next execution starts immediately after the previous one finishes (it was already due).
- fixedDelay waits a specified time after the previous execution completes before starting the next one. There is never overlap.
- cron uses a calendar expression to fire at specific times.
At National Bank of India, Rajesh uses cron to calculate interest at 2 AM daily, and fixedDelay to poll the payment gateway every 30 seconds without overlap.
package com.bank.scheduler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
@Component
public class InterestScheduler {
private static final Logger log = LoggerFactory.getLogger(InterestScheduler.class);
// Cron: runs at 2 AM every day
@Scheduled(cron = "0 0 2 * * *")
public void calculateDailyInterest() {
log.info("Starting daily interest calculation for all savings accounts");
// calculate interest for each account using BigDecimal
}
// fixedDelay: waits 30 seconds AFTER previous run completes
@Scheduled(fixedDelay = 30000)
public void pollPaymentGateway() {
log.info("Polling payment gateway for pending settlements");
// no overlap guaranteed
}
// fixedRate: fires every 60 seconds regardless of duration
@Scheduled(fixedRate = 60000)
public void refreshExchangeRates() {
log.info("Refreshing exchange rates from external API");
// may overlap if the previous run takes more than 60 seconds
}
}
package com.bank.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling // Required -- without this, all @Scheduled annotations are ignored
public class SchedulingConfig {
}
fixedRate vs fixedDelay timeline (rate/delay = 5s, task takes 8s):
fixedRate:
|--task1 (8s)--|--task2 (8s)--|--task3 (8s)--|
0s 8s 16s 24s
(task2 was due at 5s, starts immediately at 8s)
fixedDelay:
|--task1 (8s)--| 5s |--task2 (8s)--| 5s |--task3--|
0s 8s 13s 21s 26s
(always waits 5s after previous completes)
What is @Retryable and how does exponential backoff work?
@Retryable from spring-retry retries a method when it throws a specified exception. You configure the maximum number of attempts and the backoff delay. When all retries are exhausted, the @Recover method provides a fallback.
At National Bank of India, Rajesh's dividend payout service calls the core banking API, which sometimes fails with transient network errors. @Retryable retries 3 times with exponential backoff (1s, 2s, 4s) before falling back to a manual processing queue.
package com.bank.service;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
@Service
public class DividendPayoutService {
private static final Logger log = LoggerFactory.getLogger(DividendPayoutService.class);
@Retryable(
retryFor = java.net.ConnectException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public String payDividend(String accountId, BigDecimal amount) {
log.info("Attempting dividend payout: {} to account {}", amount, accountId);
return coreBankingClient.creditAccount(accountId, amount);
// Attempt 1: fails -> wait 1s
// Attempt 2: fails -> wait 2s
// Attempt 3: fails -> @Recover kicks in
}
@Recover
public String fallback(java.net.ConnectException ex, String accountId, BigDecimal amount) {
log.warn("All retries exhausted for account {}. Queuing for manual processing.", accountId);
manualQueue.enqueue(accountId, amount);
return "QUEUED_FOR_MANUAL_PROCESSING";
}
}
package com.bank.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
@Configuration
@EnableRetry // Required -- without this, @Retryable annotations are ignored
public class RetryConfig {
}
Execution log:
14:00:00 Attempting dividend payout: 5000.00 to account SAV2001
14:00:00 ConnectException: Connection refused
14:00:01 Attempting dividend payout: 5000.00 to account SAV2001 (retry 1, waited 1s)
14:00:01 ConnectException: Connection refused
14:00:03 Attempting dividend payout: 5000.00 to account SAV2001 (retry 2, waited 2s)
14:00:03 ConnectException: Connection refused
14:00:03 All retries exhausted for account SAV2001. Queuing for manual processing.
What is Flyway and how does it manage database migrations?
Flyway is a database migration tool that runs versioned SQL scripts on application startup. Each script has a version number (V1, V2, V3), and Flyway records which versions have been applied in the flyway_schema_history table. This ensures each migration runs exactly once, even across multiple deployments.
The critical rule is: never modify a migration after it has been applied. Always create a new version.
Project structure:
src/main/resources/db/migration/
V1__create_accounts_table.sql
V2__add_balance_column.sql
V3__create_transactions_table.sql
V4__add_index_on_account_id.sql
-- V1__create_accounts_table.sql
CREATE TABLE accounts (
id VARCHAR(20) PRIMARY KEY,
customer_id VARCHAR(20) NOT NULL,
account_type VARCHAR(10) NOT NULL,
status VARCHAR(10) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- V2__add_balance_column.sql
ALTER TABLE accounts ADD COLUMN balance DECIMAL(15,2) NOT NULL DEFAULT 0.00;
-- V3__create_transactions_table.sql
CREATE TABLE transactions (
id VARCHAR(36) PRIMARY KEY,
account_id VARCHAR(20) NOT NULL REFERENCES accounts(id),
amount DECIMAL(15,2) NOT NULL,
txn_type VARCHAR(10) NOT NULL,
description VARCHAR(200),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Startup log:
Flyway: Schema version: 2 (add_balance_column)
Flyway: Migrating to version 3 (create_transactions_table)
Flyway: Successfully applied 1 migration (execution time 00:00.045s)
What is Testcontainers and how do you use it for integration tests?
Testcontainers starts real Docker containers during integration tests. Instead of using an in-memory database like H2 (which behaves differently from PostgreSQL), Rajesh tests against a real PostgreSQL container that matches production.
Spring Boot 3.1+ supports @ServiceConnection, which auto-configures the DataSource to point at the Testcontainers instance.
package com.bank.repository;
import com.bank.model.Account;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class AccountRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
private AccountRepository accountRepository;
@Test
void shouldSaveAndFindAccount() {
Account account = new Account("SAV2001", "CUST001", "SAVINGS",
new BigDecimal("150000.00"));
accountRepository.save(account);
Account found = accountRepository.findById("SAV2001").orElseThrow();
assertThat(found.getBalance()).isEqualByComparingTo(new BigDecimal("150000.00"));
}
}
Test execution:
1. Testcontainers starts a PostgreSQL 16 Docker container.
2. @ServiceConnection auto-configures spring.datasource.url to point at the container.
3. Flyway migrations run against the real PostgreSQL.
4. Test executes against real PostgreSQL (not H2).
5. Container is destroyed after tests complete.
What is @Value and SpEL?
@Value injects a single property from application.yml. SpEL (Spring Expression Language) allows evaluating dynamic expressions inside @Value.
package com.bank.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class LoanConfigService {
// Simple property injection
@Value("${loan.max-amount:5000000}")
private BigDecimal maxLoanAmount;
// SpEL expression -- evaluates at runtime
@Value("#{${loan.base-rate} + ${loan.margin}}")
private BigDecimal effectiveRate;
// System property
@Value("#{systemProperties['user.timezone']}")
private String timezone;
public boolean isEligible(BigDecimal requestedAmount) {
return requestedAmount.compareTo(maxLoanAmount) <= 0;
}
}
# application.yml
loan:
max-amount: 5000000
base-rate: 8.5
margin: 1.25
effectiveRate = 8.5 + 1.25 = 9.75
maxLoanAmount = 5000000
timezone = Asia/Kolkata
What is @Lazy and when should you use it?
@Lazy defers bean creation until the bean is first used. By default, Spring creates all singleton beans at startup. This is fine for most beans, but some beans are expensive to create (like a connection to an external system that might not be needed).
package com.bank.service;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Service
@Lazy // created only when first injected or called
public class ReportGenerationService {
public ReportGenerationService() {
// expensive initialization: connects to external reporting engine
System.out.println("ReportGenerationService initialized");
}
public byte[] generateStatement(String accountId) {
// generate PDF statement for account SAV2001
return new byte[0];
}
}
Without @Lazy:
Startup: ReportGenerationService initialized (even if no one calls it)
With @Lazy:
Startup: nothing happens
First call to generateStatement(): ReportGenerationService initialized
What are @Conditional annotations?
@Conditional annotations control whether a bean is created based on conditions. Spring Boot provides several built-in variants.
package com.bank.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CacheConfig {
// Only create this bean if the property is set to true
@Bean
@ConditionalOnProperty(name = "cache.redis.enabled", havingValue = "true")
public CacheManager redisCacheManager() {
return new RedisCacheManager(); // real Redis cache
}
// Create this bean only if no other CacheManager exists
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager inMemoryCacheManager() {
return new ConcurrentMapCacheManager(); // simple in-memory fallback
}
}
# application.yml
cache:
redis:
enabled: true # uses RedisCacheManager
# enabled: false # falls back to ConcurrentMapCacheManager
With cache.redis.enabled=true:
RedisCacheManager bean is created.
ConditionalOnMissingBean: CacheManager exists, so inMemoryCacheManager is NOT created.
With cache.redis.enabled=false:
ConditionalOnProperty: redisCacheManager is NOT created.
ConditionalOnMissingBean: no CacheManager exists, so inMemoryCacheManager IS created.
What are common mistakes with these features?
The most common mistake with @Scheduled is forgetting @EnableScheduling. Without it, all @Scheduled annotations are completely ignored.
// WRONG: missing @EnableScheduling -- method never runs
@Component
public class InterestScheduler {
@Scheduled(cron = "0 0 2 * * *")
public void calculateInterest() { /* never executes */ }
}
// CORRECT: @EnableScheduling activates the scheduler
@Configuration
@EnableScheduling
public class SchedulingConfig { }
@Component
public class InterestScheduler {
@Scheduled(cron = "0 0 2 * * *")
public void calculateInterest() { /* runs at 2 AM daily */ }
}
With Flyway, a common mistake is modifying a migration file after it has been applied. Flyway checks the checksum of each file. If the checksum changes, startup fails with a validation error.
Flyway error:
Migration checksum mismatch for migration V2__add_balance_column.sql
Applied checksum: 1234567890
Resolved checksum: 9876543210
Fix: never modify applied migrations. Create V5__fix_balance_default.sql instead.
With @Retryable, forgetting @EnableRetry causes all retry annotations to be silently ignored.
What is CommandLineRunner vs ApplicationRunner?
Both run once after the application context is fully initialized. The difference is the argument type: CommandLineRunner receives raw String[] args, while ApplicationRunner receives a parsed ApplicationArguments object.
Rajesh uses CommandLineRunner to load the holiday calendar into cache on startup.
package com.bank.config;
import com.bank.service.HolidayService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Configuration
public class StartupConfig {
private static final Logger log = LoggerFactory.getLogger(StartupConfig.class);
@Bean
CommandLineRunner loadHolidayCalendar(HolidayService holidayService) {
return args -> {
int count = holidayService.loadCalendar();
log.info("Loaded {} holidays into cache", count);
};
}
@Bean
CommandLineRunner verifyDatabaseConnectivity(javax.sql.DataSource dataSource) {
return args -> {
try (var conn = dataSource.getConnection()) {
log.info("Database connectivity verified: {}", conn.getMetaData().getURL());
}
};
}
}
Startup log:
Started NbiBankingApplication in 3.2 seconds
Loaded 52 holidays into cache
Database connectivity verified: jdbc:postgresql://localhost:5432/nbi_banking
What should a senior engineer know?
- Multipart file upload safety. Validate the file type, enforce a maximum size with
spring.servlet.multipart.max-file-size, and stream the file to storage (S3, MinIO) without loading the entire file into memory. Never trust the client-provided filename. - Spring DevTools. Enables automatic restart on code changes during development and a live-reload trigger for the browser. It should never be on the production classpath.
- GraalVM native images. Ahead-of-time compilation eliminates JVM startup entirely (0.01s startup). Limitations: no runtime reflection without configuration, no dynamic class loading. Add
@RegisterReflectionForBindingfor DTOs that need reflection. - BeanDefinitionRegistryPostProcessor vs BeanPostProcessor. BeanDefinitionRegistryPostProcessor modifies bean definitions before any bean is created (e.g., registering beans programmatically). BeanPostProcessor modifies bean instances after creation.
- Spring State Machine. Models complex business workflows with explicit states and transitions. At National Bank of India, a loan application moves through states: SUBMITTED -> UNDER_REVIEW -> APPROVED -> DISBURSED (or REJECTED). The state machine enforces valid transitions.
What are common interview follow-up questions?
- What is the difference between fixedRate and fixedDelay if the task takes longer than the interval? (fixedRate: next execution starts immediately after the previous one finishes because it was already overdue. fixedDelay: always waits the full delay after the previous execution completes.)
- Can @Retryable retry on multiple exception types? (Yes, use retryFor = {ConnectException.class, TimeoutException.class}.)
- What happens if you run two instances of the same service with @Scheduled? (Both instances execute the scheduled method independently. Use ShedLock or a database lock to ensure only one instance runs the task.)
- How does Testcontainers manage container lifecycle? (With @Container, the container starts before tests and stops after. Static containers are shared across all tests in the class; instance containers are recreated per test method.)
- What is the difference between @Value and @ConfigurationProperties? (@Value injects one property at a time. @ConfigurationProperties binds an entire group of properties to a POJO, with type safety and validation.)
Visual Overview
@Scheduled Modes
════════════════
fixedRate (5s interval, task takes 8s):
├──task1(8s)──├──task2(8s)──├──task3(8s)──┤
0s 8s 16s 24s
fixedDelay (5s delay, task takes 8s):
├──task1(8s)──┤──5s──├──task2(8s)──┤──5s──├──task3──┤
0s 8s 13s 21s 26s
cron (every day at 2 AM):
──────────02:00──────────02:00──────────02:00──────
Flyway Migration Flow
═════════════════════
Application starts
│
▼
┌───────────────────────────────┐
│ Flyway checks │
│ flyway_schema_history table │
└───────────┬───────────────────┘
│
┌─────┴──────┐
│ │
▼ ▼
┌───────────┐ ┌──────────────┐
│ V1 applied│ │ V3 pending │
│ V2 applied│ │ V4 pending │
└───────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ Run V3, V4 │
│ Record in │
│ history table│
└──────────────┘
@Retryable with Exponential Backoff
════════════════════════════════════
Attempt 1 ──► FAIL ──► wait 1s
Attempt 2 ──► FAIL ──► wait 2s
Attempt 3 ──► FAIL ──► @Recover fallback
Key Takeaway
Spring provides a rich set of utility features for production banking applications. @Scheduled supports three timer modes: fixedRate fires at a constant interval, fixedDelay waits after the previous execution completes, and cron fires at calendar-based times. At National Bank of India, Rajesh uses cron to calculate daily interest at 2 AM and fixedDelay to poll the payment gateway every 30 seconds. @Retryable with exponential backoff handles transient failures: the dividend payout service retries the core banking API three times before falling back to a manual queue via @Recover.
Flyway manages database schema evolution through versioned SQL files that run on startup. The golden rule: never modify a migration after it has been applied -- always create a new version. Testcontainers ensures integration tests run against real PostgreSQL instead of H2, catching database-specific bugs before production. @Value injects properties and SpEL expressions, @Lazy defers expensive bean creation, and @ConditionalOnProperty toggles features based on configuration. CommandLineRunner loads the holiday calendar into cache on startup.
Remember one sentence: @Scheduled with @EnableScheduling for timers, @Retryable with @EnableRetry for transient failures, Flyway for migrations, Testcontainers for real database tests, and CommandLineRunner for startup tasks.
---