Wells Fargo Lead Java Engineer · Week 3 of 4

Kafka · Redis
· Oracle SQL Infrastructure depth that separates leads

Most candidates know Spring Boot. Very few can explain Kafka ordering guarantees, Redis Redlock, execution plan analysis, and isolation levels from memory. This week is your edge.

🔥 Kafka Internals Exactly-Once Consumer Groups Redis Data Structures Cache Patterns Redlock SQL Optimization Execution Plans Isolation Levels Distributed Lock
01
Part One — Apache Kafka
Kafka Architecture & Internals
Brokers, partitions, ISR, replication, consumer groups — the architecture you must visualise and explain fluently before discussing ordering or delivery semantics.
📨
Broker, Partition, ISR — Full Cluster Model
Topics · Replication · Leader election · ISR shrinkage
Critical
Kafka Cluster — payments-topic, 3 partitions, replication factor 3
Producers
payment-svc
key=accountId
transfer-svc
key=txnId
↓ partition(key.hashCode() % numPartitions)
Kafka Cluster — 3 Brokers
Broker 1
P-0 LEADER
P-1 replica
P-2 replica
Broker 2
P-0 replica
P-1 LEADER
P-2 replica
Broker 3
P-0 replica
P-1 replica
P-2 LEADER
↓ consumer group pulls from leader partitions
Consumer Group — payment-processors
Consumer-1
assigned P-0
Consumer-2
assigned P-1
Consumer-3
assigned P-2
Key Concepts
ConceptWhat It MeansInterview Depth
ISR (In-Sync Replicas)Set of replicas fully caught up to leaderMessage only committed when all ISR acks. ISR shrinks if replica falls behind by replica.lag.time.max.ms
acks=allProducer waits for all ISR to ackStrongest durability. With ISR=1 (only leader), still just one ack. Always set min.insync.replicas=2
PartitionOrdered, immutable log of messagesUnit of parallelism. More partitions = more consumers = more throughput. But more partitions = more overhead
OffsetMonotonically increasing message ID within a partitionConsumer controls its own offset. Can replay by resetting offset. Stored in __consumer_offsets topic
Consumer GroupN consumers sharing topic partition loadEach partition assigned to exactly one consumer in group. Max parallelism = num partitions
Log CompactionKafka retains only latest value per keyUsed for changelog/event sourcing. Payment status updates — latest state per paymentId retained
🎤 Interview Question

What happens to a Kafka partition when its leader broker dies?

✅ Expert Answer
  • ZooKeeper/KRaft detects broker failure (via heartbeat timeout)
  • Controller broker elects a new leader from the ISR list for each affected partition
  • Producers and consumers update their metadata and reconnect to new leader — transparent with retries
  • If ISR has only the dead leader — partition becomes unavailable (acks=all) OR an out-of-sync replica can be elected if unclean.leader.election.enable=true (risk: data loss)
  • For payments: always keep unclean.leader.election.enable=false — prefer unavailability over data loss
🔢
Kafka Ordering Guarantees
Per-partition ordering · Keyed messages · max.in.flight · Payment ordering
Most Asked
Offset Timeline — Ordering Is Per-Partition
Partition 0 — Ordered log for accountId=ACC-001
Partition-0 · payments-topic ← older | newer →
offset 0INITIATED
offset 1VALIDATED
offset 2PROCESSING
offset 3← consumer here
offset 4SETTLED (lag)
offset 5NOTIFIED (lag)

All events for ACC-001 land on Partition-0 (key=accountId) → processed in exact order

🔥 Critical Interview Question

How does Kafka guarantee ordering? How do you maintain ordering in a high-throughput payment system with retries enabled?

✅ Expert Answer

Kafka ordering is per-partition, not per-topic. Guaranteed only within a single partition.

  • Use message key: set key=accountId or key=paymentId → same key always routes to same partition via hashCode() % numPartitions
  • Ordering breaks with retries: if producer sends msg1, msg2 and msg1 fails → retry → msg2, msg1 order. Fix with max.in.flight.requests.per.connection=1 (but kills throughput)
  • Better fix — idempotent producer: enable.idempotence=true assigns sequence numbers. Broker deduplicates + reorders. Allows max.in.flight=5 safely
  • Partition count change: adding partitions breaks key→partition mapping for existing keys — all historical ordering breaks. Plan partition count upfront.
⚠️ Common Mistake

Setting retries=MAX_INT without enable.idempotence=true causes re-ordering under network failures. For a payment INITIATED→SETTLED event sequence, this is catastrophic — settlement could be processed before initiation.

PaymentProducerConfig.java — Ordering-Safe
// ✅ Ordering-safe producer for payment events
props.put("key.serializer", "StringSerializer");
props.put("enable.idempotence", "true");          // dedup + reorder at broker
props.put("acks", "all");                          // all ISR must ack
props.put("retries", Integer.MAX_VALUE);            // retry forever
props.put("max.in.flight.requests.per.connection", 5); // safe with idempotence
props.put("delivery.timeout.ms", 120000);

// Send with key = accountId → guaranteed same partition
ProducerRecord<String, PaymentEvent> record = new ProducerRecord<>(
    "payments",
    payment.getAccountId(),  // KEY — determines partition
    event                       // VALUE
);
producer.send(record, (meta, ex) -> {
    if (ex != null) alerting.trigger("kafka-send-failure", ex);
});
🎯
Exactly-Once Semantics (EOS)
At-most-once · At-least-once · Exactly-once · Transactions API
Critical for Payments
SemanticWhenRiskUse in Payments?How to Achieve
At-Most-OnceCommit offset before processingData loss on crashNeverAuto-commit before consuming
At-Least-OnceCommit offset after processingDuplicate processing on retryWith idempotent consumersDisable auto-commit, manual ack after DB write
Exactly-OnceAtomic: process + commit offsetNo loss, no duplicatesIdeal for payment debit/creditKafka Transactions API
Exactly-Once — How It Works Under the Hood
Kafka EOS — Produce + Consume + DB write as one atomic operation
1
producer.initTransactions() — register transactional.id with broker
2
producer.beginTransaction()
3
producer.send(outboundTopic, processedEvent) — buffered, not yet visible
4
producer.sendOffsetsToTransaction(offsets, consumerGroup) — atomically commit consumed offset
5
producer.commitTransaction() — both the output record AND offset become visible atomically ✓
X
On failure: producer.abortTransaction() — rolls back everything, no partial state

Consumer must set isolation.level=read_committed — only sees committed transactions

ExactlyOnceProcessor.java
@KafkaListener(topics = "payments-raw")
@Transactional("kafkaTransactionManager")
public void process(ConsumerRecord<String, PaymentEvent> record) {

    // 1. Enrich event
    PaymentEvent enriched = enrichmentService.enrich(record.value());

    // 2. Write to DB (within same Spring @Transactional)
    paymentRepo.save(enriched);

    // 3. Produce to next topic — same Kafka transaction
    kafkaTemplate.send("payments-processed", record.key(), enriched);

    // Kafka TX Manager commits: offset + output message atomically
    // If anything fails → abort → consumer re-processes from last committed offset
}

// application.yml — exactly-once config
// spring.kafka.producer.transaction-id-prefix: payment-proc-
// spring.kafka.consumer.isolation-level: read_committed
// spring.kafka.producer.acks: all
// spring.kafka.producer.enable-idempotence: true
👥
Consumer Groups, Rebalancing & Lag
Partition assignment · Rebalance triggers · Lag monitoring · Dead letter queue
Important
ScenarioWhat HappensImpact
Consumer joins groupRebalance triggered — all partitions reassignedBrief pause in consumption during rebalance
Consumer crashesMissed heartbeat → group coordinator triggers rebalancePartitions reassigned to healthy consumers
More consumers than partitionsExtra consumers sit idleNo benefit — max parallelism = partition count
Slow consumerLag grows → alert triggersBackpressure, OOM risk if lag unbounded
max.poll.interval.ms exceededConsumer presumed dead → rebalanceCommon when processing is slow — tune the interval or reduce batch size
🎤 Interview Question

How would you handle a poison pill message (one that always causes the consumer to crash)?

✅ Production Answer
  • Try-catch in consumer: catch deserialisation/processing errors, don't throw — prevents endless retry loop
  • Dead Letter Topic (DLT): on failure after N retries, produce the bad message to payments.DLT — preserve it for analysis without blocking main topic
  • Spring Kafka @RetryableTopic: handles retry + DLT automatically with exponential backoff. Retries go to payments-retry-0, payments-retry-1… final fail → DLT
  • Alert on DLT: monitor DLT size — any message in DLT = P2 alert in payments
  • Never commit a failed offset: that permanently skips the message
RetryableTopic + DLT.java
@RetryableTopic(
    attempts = "4",
    backoff = @Backoff(delay = 1000, multiplier = 2.0, maxDelay = 30000),
    dltStrategy = DltStrategy.FAIL_ON_ERROR,
    topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE,
    autoCreateTopics = "true"
)
@KafkaListener(topics = "payments")
public void process(PaymentEvent event) {
    paymentProcessor.handle(event); // retried: payments-retry-0 → -1 → -2 → DLT
}

@DltHandler
public void handleDlt(PaymentEvent event, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
    log.error("DLT received from {}: {}", topic, event);
    alerting.page("PAYMENT_DLT", event.getPaymentId());
    dltRepo.save(new FailedPayment(event, topic)); // persist for ops team
}
02
Part Two — Redis
Redis Data Structures & Caching Patterns
Redis is more than a cache. Know its data structures, eviction policies, and the cache problems that trip up payment systems under load.
🗃️
Redis Data Structures — All 8 Types
String · Hash · List · Set · ZSet · HLL · Stream · Geo
Important
String
SET / GET / INCR / SETNX
  • Idempotency keys with TTL
  • Session tokens
  • Counter (atomic INCR)
  • Rate limiting per user
Hash
HSET / HGET / HMGET
  • User session object fields
  • Payment metadata cache
  • Config key-value pairs
  • Memory efficient for objects
List
LPUSH / RPOP / LRANGE
  • Task queue (producer-consumer)
  • Recent transaction history
  • Capped log (LTRIM)
  • Activity feed
Set
SADD / SISMEMBER / SMEMBERS
  • Unique visitors per day
  • Fraud flag tags per account
  • Set intersections (common friends)
  • Deduplication window
Sorted Set
ZADD / ZRANGE / ZRANGEBYSCORE
  • Leaderboards / rankings
  • Rate limiting with sliding window
  • Scheduled jobs (score=timestamp)
  • Priority queues
Stream
XADD / XREAD / XACK
  • Kafka-like log in Redis
  • Consumer groups
  • Audit event stream
  • Real-time activity feed
HyperLogLog
PFADD / PFCOUNT
  • Approx unique count (0.81% error)
  • Daily active users at scale
  • 12KB fixed memory
  • Count unique payment IDs seen
Geo
GEOADD / GEODIST / GEORADIUS
  • Nearby ATM/branch lookup
  • Location-based fraud detection
  • Distance calculations
Cache Patterns + Cache Failure Modes
Cache-aside · Write-through · Cache stampede · Penetration · Avalanche
Critical
Cache-Aside
(Lazy Loading)
1. Read from cache
2. MISS → read from DB
3. Write to cache + return
✓ Best for read-heavy
Write-Through
(Sync write)
1. Write to cache
2. Write to DB (same op)
✓ Always consistent
✗ Higher write latency
Write-Behind
(Async write)
1. Write to cache
2. Async write to DB later
✓ Lowest write latency
✗ Data loss risk — not for payments
Read-Through
(Cache loads itself)
1. App always reads cache
2. Cache fetches DB on miss
✓ Transparent to app
Used in Redis + DB proxies
Cache Failure Modes — Know These for Payments
ProblemWhat HappensSolution
Cache StampedePopular key expires → 1000 requests all miss cache simultaneously → all hit DB → DB collapsesMutex lock on miss (only one thread populates), or probabilistic early expiry (refresh before expiry)
Cache PenetrationRequests for non-existent keys (e.g. invalid paymentId) bypass cache → hit DB every timeCache null values with short TTL, or Bloom filter at edge to reject impossible keys
Cache AvalancheMany keys expire at same time → mass DB requests → DB overloadJittered TTL: TTL = base + random(0, variance), stagger expiry, Redis persistence warmup
Cache InconsistencyDB updated but cache not invalidated → stale readsUse Cache-aside with write-on-update, versioned cache keys, or event-driven invalidation via Kafka
🎤 Interview Question

A payment's balance endpoint is getting 50,000 RPS during peak hours. When the Redis TTL expires, your Oracle DB gets a thundering herd. How do you solve this?

✅ Production Answer — Three Layers
  • Probabilistic early refresh: at 80% of TTL, randomly start refreshing — spreads load, no hard expiry cliff
  • Distributed mutex (SETNX): on cache miss, first thread acquires lock, loads DB, populates cache. Other threads wait briefly then read from cache. Prevents N threads hitting DB simultaneously.
  • Stale-while-revalidate: serve stale cached value while async refresh runs in background — zero latency impact to user
  • Jitter on TTL: instead of TTL=300 for all balance keys, use TTL = 300 + random(0, 30)
Redis Eviction Policies — Know for Memory Management
noeviction
Returns error when memory full. No eviction.
Use: critical data you can't lose
allkeys-lru
Evict least recently used keys from all keys.
Use: general cache — most common choice
volatile-lru
Evict LRU among keys with TTL set only.
Use: mixed persistent + cache data
allkeys-lfu
Evict least frequently used keys.
Use: hot/cold data access patterns
volatile-ttl
Evict key with shortest remaining TTL.
Use: when TTL represents priority
allkeys-random
Random eviction.
Use: uniform access, no hot keys
🔐
Distributed Locking with Redis (Redlock)
SETNX · Redlock algorithm · Redisson · Expiry traps
Critical for Payments
Distributed Lock Flow — Step by Step
Redis Distributed Lock — Preventing Duplicate Payment Processing
1️⃣
SET lock:pay-{id} {uniqueUUID} NX PX 30000
NX=only if not exists · PX=expire ms · value=owner UUID
2️⃣
Lock acquired (SET returns OK) → process payment safely
3️⃣
Lock NOT acquired (SET returns nil) → another process holds it → retry / return error
4️⃣
Release: Lua script → GET lock, if value==UUID then DEL
Must verify UUID before delete — prevent releasing another process's lock
⏱️
TTL auto-expires lock if process crashes — prevents deadlock forever
🎤 Interview Question

What is the Redlock algorithm? Why is single-node Redis lock insufficient for HA?

✅ Expert Answer

Single-node problem: if Redis master dies after granting lock but before replicating to replica, replica promotes → two processes think they hold the lock.

Redlock: acquire lock on N independent Redis nodes (typically 5). Lock is valid only if acquired on majority (≥3) within TTL. No single node failure breaks the guarantee.

  • Try to acquire on all 5 nodes with same key + UUID + TTL
  • Count successes. If ≥3 acquired within TTL window → lock held
  • Effective TTL = original TTL minus time taken to acquire
  • On failure or timeout → release all acquired locks

Practical: Use Redisson library — implements Redlock correctly with watchdog (auto-extends TTL while lock holder is alive).

⚠️ Martin Kleppmann's Critique

Even Redlock has edge cases under GC pauses and clock drift. For financial systems requiring fencing tokens (e.g. DB row version checks), use SELECT FOR UPDATE (optimistic/pessimistic DB locking) as the final safety net alongside Redis locking.

RedissonDistributedLock.java
@Service
public class PaymentLockService {

    @Autowired private RedissonClient redisson;

    public void processWithLock(String paymentId) throws InterruptedException {
        RLock lock = redisson.getLock("payment:lock:" + paymentId);
        boolean acquired = lock.tryLock(100, 30_000, TimeUnit.MILLISECONDS);
        // tryLock(waitTime, leaseTime, timeUnit)
        // leaseTime=30s auto-expires even if process crashes

        if (!acquired) {
            throw new DuplicatePaymentException("Payment "  + paymentId + " in progress");
        }
        try {
            processPayment(paymentId);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock(); // ALWAYS release in finally
            }
        }
    }
}
03
Part Three — Oracle SQL
SQL Indexing & Query Optimization
Payment tables grow to hundreds of millions of rows. The difference between a 2ms query and a 30-second full table scan is index design and statistics. Know this cold.
📊
Indexing Deep Dive — B-Tree, Composite, Covering
Index types · Selectivity · Composite column order · Index skip scan
Critical
B-Tree Index — How a Lookup Works
❌ Full Table Scan — No Index
Row 1
compare account_id
Row 2
compare account_id
Row 3
compare account_id
… 50M rows scanned …
Cost: O(N) — seconds at 50M rows
✓ B-Tree Index Lookup
Root
branch node
Branch
leaf node
Leaf
ROWID → fetch row
Height ≈ 3–4 levels
Cost: O(log N) — milliseconds
Index TypeWhen to UseExample in PaymentsWatch Out
B-Tree (default)Equality + range queries, < > BETWEENaccount_id, payment_dateNot for low-cardinality (status)
BitmapLow cardinality columns, DW queriespayment_status (5 values)Row lock issues — bad for OLTP writes
CompositeMulti-column WHERE clauses(account_id, payment_date)Column order critical — leading edge rule
Function-BasedQueries on UPPER(), TRUNC(), expressionsUPPER(beneficiary_name)Must match exact function in query
Covering IndexAll query columns in index — no table accessIndex on (account_id, amount, status)Larger index; worth it for hot queries
🎤 Interview Question

You have a composite index on (account_id, payment_date, status). Which queries use it and which don't?

✅ Answer — The Leading Edge Rule

Composite index is used left-to-right. It stops at the first missing or range column.

Query WHERE clauseIndex Used?Why
account_id = 'X'YesHits leading column
account_id = 'X' AND payment_date = DATEYes (both columns)Both leading columns used
account_id = 'X' AND payment_date > DATEPartial — stops at rangestatus not used after range on date
payment_date = DATENoSkips leading column account_id
account_id = 'X' AND status = 'SETTLED'PartialSkips date; Oracle may index skip scan

Oracle 9i+ supports Index Skip Scan — can use composite index even if leading column skipped, but only if leading column has low cardinality. Verify with EXPLAIN PLAN.

🔍
Execution Plans & Query Tuning
EXPLAIN PLAN · DBMS_XPLAN · Full scan vs index · Statistics
Must Know
EXPLAIN PLAN — Reading the Plan (Bottom-Up execution)
SELECT STATEMENT Cost=12
HASH JOIN Cost=12
INDEX RANGE SCAN idx_payment_account Cost=3
TABLE ACCESS BY ROWID accounts Cost=4

vs. the BAD plan:

SELECT STATEMENT Cost=148,902
MERGE JOIN CARTESIAN Cost=148,902
TABLE ACCESS FULL payments (50M rows!) Cost=143,000
🎤 Interview Question

A payment query was running in 50ms but after 3 months with 10M more rows it now takes 45 seconds. What's your systematic investigation?

✅ Systematic DBA-Level Answer
  • Step 1 — Capture current plan: EXPLAIN PLAN FOR <query> + SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY). Look for FULL TABLE SCAN on large tables.
  • Step 2 — Check statistics: stale optimizer stats cause bad plans. SELECT last_analyzed FROM user_tables WHERE table_name='PAYMENTS'. If old → EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA','PAYMENTS')
  • Step 3 — Implicit type conversion: WHERE account_id = 12345 on VARCHAR2 column → full scan. Must match data types.
  • Step 4 — Index usage: query using UPPER(col), TRUNC(date_col), or leading wildcard LIKE '%abc'? These skip B-tree indexes.
  • Step 5 — Index fragmentation: high delete volume → index fragmentation. ALTER INDEX idx_name REBUILD ONLINE
  • Step 6 — Lock contention: SELECT * FROM V$LOCK l JOIN V$SESSION s ON l.SID=s.SID
  • Step 7 — Bind variable peeking: Oracle optimises plan for first value seen — skewed data can cause bad plan for other values. Use CURSOR_SHARING=FORCE or hints.
QueryTuning.sql
-- 1. Capture execution plan with actual statistics
EXPLAIN PLAN FOR
SELECT p.payment_id, p.amount, a.account_name
FROM payments p
JOIN accounts a ON p.account_id = a.account_id
WHERE p.account_id = 'ACC-001'
  AND p.payment_date >= TRUNC(SYSDATE) - 30
  AND p.status = 'SETTLED';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(null, null, 'ALLSTATS LAST'));

-- 2. Refresh statistics
EXEC DBMS_STATS.GATHER_TABLE_STATS(
    ownname     => 'PAYMENTS_SCHEMA',
    tabname     => 'PAYMENTS',
    estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
    method_opt  => 'FOR ALL INDEXED COLUMNS SIZE AUTO',
    cascade     => TRUE
);

-- 3. Optimal composite index for this query pattern
-- Column order: equality first (account_id), then range (payment_date), then IN (status)
CREATE INDEX idx_pay_account_date_status
    ON payments (account_id, payment_date, status)
    TABLESPACE indx_ts
    ONLINE; -- no table lock in production!

-- 4. Covering index — avoids table access entirely
CREATE INDEX idx_pay_covering
    ON payments (account_id, payment_date, status, payment_id, amount)
    ONLINE;
🛡️
Transaction Isolation Levels
Dirty read · Non-repeatable · Phantom · READ_COMMITTED vs SERIALIZABLE
Critical
LevelDirty ReadNon-RepeatablePhantom ReadOracle Default?Use In Payments
READ UNCOMMITTEDYesYesYesNoNever — reads uncommitted garbage
READ COMMITTEDNoYesYes✓ Oracle defaultStandard for most payment reads
REPEATABLE READNoNoYesNot native Oracle (use FOR UPDATE)Consistent reporting windows
SERIALIZABLENoNoNoNo (explicit opt-in)Balance transfers, reconciliation
Isolation Problems — Visual
Non-Repeatable Read — Causes Wrong Balance Display
Transaction 1 (Report)
Transaction 2 (Payment)
T1
READ balance=1000
T2
UPDATE balance=800
T3
COMMIT
T4
READ balance=800 ≠ 1000

Under READ_COMMITTED: T1 reads different values within same transaction. Fixed by REPEATABLE_READ or SERIALIZABLE.

🎤 Interview Question

Two simultaneous payment requests try to withdraw £500 from an account with £600 balance. Both pass the balance check. How do you prevent this at the DB level?

✅ Answer — Lost Update Problem

Classic lost update / race condition. Three approaches, choose based on contention:

  • Pessimistic Locking: SELECT balance FROM accounts WHERE id=? FOR UPDATE — acquires row lock. Second transaction waits. Prevents any interleaving. Best for high-contention accounts.
  • Optimistic Locking: add version column. UPDATE accounts SET balance=?, version=version+1 WHERE id=? AND version=? — if 0 rows updated, someone else changed it → retry. Best for low-contention.
  • SERIALIZABLE isolation: DB detects write conflict, rolls back one transaction automatically. Higher overhead but fully automatic.

In production payments: use pessimistic locking (SELECT FOR UPDATE) for balance debit operations. Speed matters less than correctness for money movement.

🔒
Optimistic vs Pessimistic Locking
SELECT FOR UPDATE · Version column · Deadlock detection · Connection pool
Critical for Payments
Pessimistic Locking
SELECT … FOR UPDATE
  • Acquires row lock immediately
  • Other transactions wait or timeout
  • Prevents concurrent modification guaranteed
  • Higher contention / lower throughput
  • Risk: deadlock between transactions
  • Use: balance debit, payment settlement
Optimistic Locking
version / timestamp column
  • No lock on read — assume no conflict
  • At UPDATE: check version matches
  • If version mismatch → retry
  • Higher throughput, lower contention
  • Risk: many retries under high contention
  • Use: account metadata, preference updates
LockingPatterns.java (Spring Data JPA)
// Pessimistic locking — for balance debit
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdForUpdate(@Param("id") Long id);

// Usage
@Transactional
public void debit(Long accountId, BigDecimal amount) {
    Account acc = repo.findByIdForUpdate(accountId)
        .orElseThrow(() -> new AccountNotFoundException(accountId));
    // Row locked — no concurrent debit possible until commit
    acc.debit(amount); // validates and updates balance
    repo.save(acc);
}

// Optimistic locking — for account metadata updates
@Entity
public class AccountPreferences {
    @Version
    private Long version; // JPA auto-checks on update
    // If version mismatch → OptimisticLockException → @Retryable
}

// HikariCP connection pool — tuning for payments
// spring.datasource.hikari.maximum-pool-size=20
// spring.datasource.hikari.minimum-idle=5
// spring.datasource.hikari.connection-timeout=30000  ← fail fast, don't queue forever
// spring.datasource.hikari.idle-timeout=600000
// spring.datasource.hikari.max-lifetime=1800000
Q
Quick-Fire Round
Week 3 — Full Question Bank
All high-probability Kafka, Redis, and SQL questions for a Lead-level interview. Practise these out loud — fluency = confidence.
🎤
30+ Questions with Answer Skeletons
Kafka · Redis · Oracle SQL · Cross-topic architecture
Practice These
Kafka
Q1

What is the difference between a Kafka topic and a queue (e.g. ActiveMQ)?

Answer

Queue: message consumed by one consumer → deleted. Kafka topic: message retained on disk for configurable period. Multiple independent consumer groups can read the same message. Kafka is a durable, replayable log — fundamental architectural difference. Enables event sourcing, audit replay, new consumer groups processing historical data.

Q2

How do you design a Kafka topic for 1 million payment events per second?

Answer
  • Partition count: 100+ partitions (each partition ~10K msg/s throughput). Can scale consumers up to 100.
  • Replication factor: 3 — survive 2 broker failures
  • Batch producer: linger.ms=5, batch.size=65536 — batch messages before send
  • Compression: compression.type=lz4 — reduces network I/O dramatically
  • Consumer tuning: fetch.min.bytes, fetch.max.wait.ms — batched polling
  • Separate broker disks: dedicated SSDs per Kafka broker for log segments
Q3

What is Kafka Log Compaction? When would you use it for payments?

Answer

Log compaction retains only the latest value per key — older records with the same key are deleted during compaction. Useful for payment status topic: key=paymentId, value=latest status. Any consumer joining later gets current state without replaying full history. Used for account balance snapshots, configuration change topics.

Q4

What is consumer lag and how do you monitor and alert on it?

Answer

Consumer lag = (latest partition offset) - (consumer's committed offset) = number of unprocessed messages. Monitored via: kafka-consumer-groups.sh --describe, Kafka JMX metrics, Prometheus + kafka-exporter, Grafana dashboard. Alert when lag exceeds threshold (e.g. >10,000 for payments). Growing lag = consumer bottleneck → scale consumers up to partition count, or optimise processing, or batch DB writes.

Q5

Difference between Kafka and database change data capture (CDC)? When use CDC over Kafka producer?

Answer

CDC (Debezium + Kafka Connect) reads DB transaction log and publishes every change event. No code changes needed — captures ALL DB changes including those from batch jobs, legacy apps, DBA scripts. Use CDC when: can't modify existing apps, need dual-write consistency, or migrating from monolith. Use producer when you control the write path and want rich event payloads.

Redis
Q6

How is Redis single-threaded yet so fast?

Answer

Redis 6+ uses I/O threads for network I/O but single thread for command execution — eliminates lock overhead entirely. Speed comes from: all data in RAM (microsecond access), simple data structures, epoll/kqueue async I/O, no serialisation overhead for most operations. 100,000+ ops/sec on commodity hardware.

Q7

Redis persistence — RDB vs AOF. Which for payments?

Answer
  • RDB (Snapshot): point-in-time dump to disk periodically. Fast restore. Data loss between snapshots on crash.
  • AOF (Append-Only File): logs every write command. Can sync every second or every write. Near-zero data loss.
  • For payments: AOF with fsync=everysec — max 1 second data loss acceptable for cache. For distributed lock keys: must use TTL as safety net anyway.
  • Best: both RDB + AOF — RDB for fast restart, AOF for durability. Redis Enterprise/Sentinel for HA.
Q8

How would you implement rate limiting for payment API at 1000 req/min per user using Redis?

Answer
  • Fixed window: INCR rate:userId:minute → check <= 1000 → EXPIRE 60s. Simple but burst at window boundary.
  • Sliding window: Sorted Set. Key=userId, score=timestamp, value=requestId. ZADD new request, ZREMRANGEBYSCORE old entries, ZCARD to count. More accurate, slightly more overhead.
  • Token bucket: Store tokens in Redis. Replenish at fixed rate. Consume on each request. Smoothest limiting.
Oracle SQL
Q9

What is an N+1 query problem? How do you detect and fix it in Spring Data JPA?

Answer

Loading N accounts and for each lazily loading payments: 1 query for accounts + N queries for payments = N+1 queries. Detected via SQL logging or Hibernate statistics. Fix: @Query("SELECT a FROM Account a JOIN FETCH a.payments WHERE...") — single JOIN query. Or @EntityGraph. Or @BatchSize(size=50) for secondary fetch. Spring JPA + lazy loading is the #1 cause of slow payment reports.

Q10

What is a connection pool exhaustion? How do you diagnose it?

Answer

All pool connections in use — new threads wait. Symptoms: HikariPool-1 - Connection is not available, request timed out after 30000ms in logs. Causes: long-running transactions, missing connection release (forgotten close), too many threads. Diagnose: SELECT * FROM V$SESSION WHERE WAIT_CLASS='User I/O'. Fix: tune maximum-pool-size, reduce transaction scope, add @Transactional only where needed, alert on pool saturation metric.

Q11

Explain MVCC (Multi-Version Concurrency Control) in Oracle.

Answer

Oracle doesn't use read locks. Instead it maintains multiple versions of data via undo segments. A reading transaction sees a consistent snapshot as-of its query start time — even if another transaction is updating the same rows. This is why Oracle's READ_COMMITTED doesn't block readers — writers never block readers, readers never block writers. Undo segments store old versions. Long-running queries can get ORA-01555 (snapshot too old) if undo is recycled before query completes.