Definition
- Measure-first discipline – Always profile and quantify the slowness before changing anything, because intuition about performance is famously unreliable.
- Targeted fix – Change only the specific code path the profiler identifies as the bottleneck, not a scattershot of micro-optimizations.
- Before-and-after proof – Verify the improvement with the same metric you used to diagnose, producing a concrete number.
- Correctness guard – Confirm that the faster version still produces correct results, especially with money.
- Honest trade-offs – If the fix introduces a trade-off such as caching with stale data, state it.
Imagine customers complaining that the loan-approval desk at National Bank of India is slow. A guesser hires more staff randomly. A professional first measures where the time goes : forms take two minutes, but the credit check takes twenty : then fixes the credit check specifically. The improvement is measured: approvals go from twenty-five minutes to seven. That measure-first, fix-the-bottleneck discipline is exactly what this question rewards.
Understanding the Concepts
What is this question really testing?
The interviewer wants to see the discipline of measurement, not just the cleverness of the fix. Starting with "I measured the p99 latency at three seconds, breaching our one-second SLA" immediately signals seniority. Starting with "I thought it was slow so I added caching" signals guessing.
package com.bank.service;
import java.util.logging.Logger;
public class MeasureFirst {
private static final Logger log = Logger.getLogger(MeasureFirst.class.getName());
public static void main(String[] args) {
long p99Before = 3000; // ms
long sla = 1000; // ms
log.info(() -> "p99=" + p99Before + "ms, SLA=" + sla + "ms -> BREACH");
}
}
p99=3000ms, SLA=1000ms -> BREACH
How do you find the real bottleneck?
In a banking backend at National Bank of India, the usual suspects are database queries (N+1 patterns, missing indexes), unnecessary serialization, lock contention, or synchronous calls that should be parallel. You profile and look at where the time actually goes, not where you assume it goes.
package com.bank.service;
import java.util.List;
import java.util.logging.Logger;
public class BottleneckProfile {
private static final Logger log = Logger.getLogger(BottleneckProfile.class.getName());
public static void main(String[] args) {
// Simulated profiling result
List<String> breakdown = List.of(
"Database queries: 2700ms (90%)",
"Serialization: 200ms (7%)",
"Business logic: 100ms (3%)"
);
breakdown.forEach(b -> log.info(b));
log.info("Bottleneck: database queries");
}
}
Database queries: 2700ms (90%)
Serialization: 200ms (7%)
Business logic: 100ms (3%)
Bottleneck: database queries
What is the classic N+1 query problem?
The N+1 pattern is the most common performance problem in banking backends. Loading a list and then querying inside a loop fires one query per item. For five hundred accounts, that is over five hundred round-trips.
package com.bank.service;
import java.util.List;
public class NPlus1Problem {
// WRONG: N+1 queries
public void loadStatementSlow(List<String> accountIds) {
for (String id : accountIds) {
// Each call is a separate DB round-trip
Object txns = findTransactionsByAccount(id);
}
}
// CORRECT: single batched query
public void loadStatementFast(List<String> accountIds) {
Object allTxns = findTransactionsByAccountIds(accountIds);
}
private Object findTransactionsByAccount(String id) { return null; }
private Object findTransactionsByAccountIds(List<String> ids) { return null; }
}
How do you fix a missing index?
If the profile shows ninety percent of time in one query, adding an index turns a full table scan into an index seek. For Priya's account statement query, indexing the account_id column makes the lookup near-instant.
-- Before: full table scan on 10M rows
SELECT * FROM transactions WHERE account_id = 'ACC-2001';
-- Fix: add index
CREATE INDEX idx_txn_account ON transactions(account_id);
-- After: index seek, sub-millisecond
SELECT * FROM transactions WHERE account_id = 'ACC-2001';
package com.bank.service;
import java.util.logging.Logger;
public class IndexImprovement {
private static final Logger log = Logger.getLogger(IndexImprovement.class.getName());
public static void main(String[] args) {
long beforeMs = 2700;
long afterMs = 3;
log.info(() -> "Query time: " + beforeMs + "ms -> " + afterMs + "ms (index added)");
}
}
When is caching appropriate?
Caching is appropriate when the data tolerates slight staleness. A balance lookup for a dashboard can use a short-TTL cache, but a balance check before a debit must always read the latest value.
package com.bank.service;
import java.math.BigDecimal;
import java.util.concurrent.ConcurrentHashMap;
public class BalanceCache {
private final ConcurrentHashMap<String, BigDecimal> cache = new ConcurrentHashMap<>();
private static final long TTL_MS = 5000; // 5 seconds
// Dashboard: cache OK (staleness tolerated)
public BigDecimal getBalanceForDisplay(String accountId) {
return cache.getOrDefault(accountId, fetchFromDb(accountId));
}
// Transfer: always fresh (correctness required)
public BigDecimal getBalanceForDebit(String accountId) {
return fetchFromDb(accountId);
}
private BigDecimal fetchFromDb(String accountId) {
return new BigDecimal("50000.00"); // simulated
}
}
How do you verify without breaking correctness?
A faster wrong answer is worse than a slow right one, especially with money. Run the optimized path against the same inputs and confirm identical outputs before deploying.
package com.bank.service;
import java.util.logging.Logger;
public class CorrectnessCheck {
private static final Logger log = Logger.getLogger(CorrectnessCheck.class.getName());
public static void main(String[] args) {
String slowResult = "[TXN-1, TXN-2, TXN-3]";
String fastResult = "[TXN-1, TXN-2, TXN-3]";
boolean match = slowResult.equals(fastResult);
log.info(() -> "Correctness verified: " + match);
}
}
Correctness verified: true
How do you present the before-and-after result?
State the improvement as a business-relevant number. "p99 dropped from 3000ms to 180ms" is proof. "It got faster" is not.
package com.bank.service;
import java.util.logging.Logger;
public class BeforeAfter {
private static final Logger log = Logger.getLogger(BeforeAfter.class.getName());
public static void main(String[] args) {
long p99Before = 3000;
long p99After = 180;
double improvement = (1.0 - (double) p99After / p99Before) * 100;
log.info(() -> String.format("p99: %dms -> %dms (%.0f%% improvement)", p99Before, p99After, improvement));
}
}
p99: 3000ms -> 180ms (94% improvement)
What should a senior engineer emphasize about trade-offs?
Always mention the trade-off honestly. If you added caching, mention the staleness window. If you batched queries, mention the memory trade-off of loading more data at once. Honesty about costs is what separates a senior answer from a junior one.
package com.bank.service;
public class TradeOffDisclosure {
public static void main(String[] args) {
System.out.println("Fix: added 5s cache for balance display");
System.out.println("Trade-off: dashboard may show balance up to 5s stale");
System.out.println("Acceptable: display only, not used for debits");
}
}
Visual Overview
┌────────────────────────────────────────────────────────┐ │ Performance Improvement Workflow │ ├────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────┐ │ │ │ 1. MEASURE │ p99 = 3000ms (SLA = 1000ms) │ │ └────────┬─────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 2. PROFILE │ DB queries = 90% of time │ │ └────────┬─────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 3. FIX │ Batch N+1 → single query │ │ │ (targeted) │ + add missing index │ │ └────────┬─────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 4. VERIFY │ Correctness: outputs match │ │ │ │ Performance: p99 = 180ms │ │ └────────┬─────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 5. REPORT │ 94% improvement, SLA met │ │ │ (before/after) │ Trade-off: none in this case │ │ └──────────────────┘ │ └────────────────────────────────────────────────────────┘
Key Takeaway
The slow system I improved at National Bank of India was the account-statement screen that customers like Rajesh and Priya use daily. Customers were complaining that pulling up a statement took around three seconds, which breached our one-second latency target, and it was worst for customers with long transaction histories. I owned the fix. The first thing I did was measure rather than guess. I profiled the endpoint and the data was unambiguous: almost all the time was spent in the database, not in the application logic.
When I looked closer, it was a classic N+1 query problem. To build the statement, the code loaded the account and then fired a separate database query for every single transaction. For a customer with five hundred transactions, that was over five hundred round-trips to the database, and each round-trip has a cost that adds up quickly. The fix was targeted at exactly that bottleneck: instead of querying per transaction, I batched them into a single query that fetched all the transactions at once.
The improvement was dramatic and, importantly, measured. The p99 for that endpoint dropped from three thousand milliseconds to around one hundred and eighty, comfortably under our SLA. I verified the output was identical before and after, because a faster but wrong statement would be far worse than a slow correct one, especially with money.
The reason I tell it this way is that performance work lives or dies on discipline. I always measure before I change anything, because intuition about performance is unreliable and it is easy to waste days optimizing code that was never the bottleneck. I fix the specific thing the profiler points to, verify with real before-and-after numbers, and I am honest about any trade-off.
Remember one sentence: Measure first, fix the real bottleneck the profiler shows you, and prove the improvement with before-and-after numbers.
---