Careeroza
Real World Example
Speeding Up a Slow "My Orders" Page
A typical performance ticket at an e-commerce company: "My Orders" takes 4 seconds for users with many orders. The fix is a series of small, measured steps.
| Step | Finding | Fix | p95 latency |
|---|---|---|---|
| 0. Measure | Actuator + APM: 92% of time in SQL | — | 4.1 s |
| 1. N+1 queries | 1 query for orders + 1 per order for items (200 queries) | JOIN FETCH / batch fetch | 1.2 s |
| 2. No pagination | loads all 600 orders | Pageable, 20 per page | 310 ms |
| 3. Missing index | full scan on orders.user_id | CREATE INDEX idx_orders_user ON orders(user_id, created_at) | 90 ms |
| 4. Repeated product lookups | same product data fetched per request | cache with TTL (Caffeine / Redis) | 45 ms |
The N+1 Problem in Plain Java
Example
A simulated repository counts "queries". Loading items per order costs 1 + N queries; one bulk query grouped in memory costs 2.
import java.util.*;
import java.util.stream.*;
public class Main {
static int queries = 0;
static final List<Item> ITEMS = IntStream.range(0, 60)
.mapToObj(i -> new Item(i % 20, "item-" + i, 100 + i)).toList();
static List<Integer> findOrderIds(int userId) { queries++; return IntStream.range(0, 20).boxed().toList(); }
static List<Item> findItemsByOrder(int orderId) {
queries++; return ITEMS.stream().filter(i -> i.orderId() == orderId).toList();
}
static List<Item> findItemsByOrders(Collection<Integer> ids) {
queries++; return ITEMS.stream().filter(i -> ids.contains(i.orderId())).toList();
}
public static void main(String[] args) {
queries = 0;
Map<Integer, Integer> slow = new TreeMap<>();
for (int id : findOrderIds(7)) slow.put(id, findItemsByOrder(id).stream().mapToInt(Item::price).sum());
System.out.println("N+1 approach: " + queries + " queries");
queries = 0;
List<Integer> ids = findOrderIds(7);
Map<Integer, Integer> fast = findItemsByOrders(new HashSet<>(ids)).stream()
.collect(Collectors.groupingBy(Item::orderId, TreeMap::new, Collectors.summingInt(Item::price)));
System.out.println("Bulk approach: " + queries + " queries");
System.out.println("Same totals: " + slow.equals(fast) + ", order 0 total = " + fast.get(0));
}
}
record Item(int orderId, String name, int price) {}Output
N+1 approach: 21 queries Bulk approach: 2 queries Same totals: true, order 0 total = 360
Takeaways
- Measure first; fix the biggest bottleneck; measure again.
- Database access patterns (N+1, indexes, pagination) beat micro-optimizations every time.
- Cache read-heavy, rarely-changing data with a TTL and a size limit.
- Write a regression test or a load test (Gatling, k6, JMeter) so the page does not get slow again.