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.

StepFindingFixp95 latency
0. MeasureActuator + APM: 92% of time in SQL—4.1 s
1. N+1 queries1 query for orders + 1 per order for items (200 queries)JOIN FETCH / batch fetch1.2 s
2. No paginationloads all 600 ordersPageable, 20 per page310 ms
3. Missing indexfull scan on orders.user_idCREATE INDEX idx_orders_user ON orders(user_id, created_at)90 ms
4. Repeated product lookupssame product data fetched per requestcache 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.