Week 8 — Production Patterns: Pagination, Async, Monitoring
Deep dives for this week
Rate limiting’s sliding window is DSA 04; pagination’s SQL reality is in the SQL capstone; timeouts, retries, and connection pools — the production patterns this week only gestures at — are Networking 06.
This Week’s Theme
Your project should stop looking like a tutorial and start looking like production software. Real apps have pagination, rate limiting, structured logging, and health checks.
Daily Breakdown
Day 1: Pagination — Real Data Is Big
Never return unbounded lists. A group with 10,000 expenses will crash the client.
Two approaches:
Offset-based (simple, fine for most cases):
@GetMapping
public Page<ExpenseDTO> getExpenses(
@PathVariable Long groupId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
return expenseRepo.findByGroupId(groupId, pageable).map(this::toDTO);
}
Response:
{
"content": [...],
"totalElements": 1547,
"totalPages": 78,
"number": 0,
"size": 20,
"first": true,
"last": false
}
Cursor-based (better for real-time feeds):
GET /expenses?cursor=2024-01-15T10:30:00&limit=20
- No “page 47” — just “give me 20 items after this timestamp”
- Doesn’t break when new items are inserted (offset pagination does)
- Used by Twitter, Slack, most real-time feeds
Implement offset pagination this week. Know cursor pagination exists for interviews.
Day 2: Rate Limiting
Why: Without rate limiting, one bad actor can overload your entire system.
Approaches:
| Algorithm | How It Works | Pros | Cons |
|---|---|---|---|
| Fixed Window | 100 requests per minute, window resets | Simple | Burst at window boundary |
| Sliding Window | 100 requests in any 60-second window | Smooth | More complex |
| Token Bucket | Bucket fills at steady rate, each request takes a token | Handles bursts gracefully | Slightly complex |
The sliding window row is exactly the pattern from DSA 04 — Sliding Window — same subtract-what-leaves, add-what-enters mechanic, applied to requests-per-minute instead of subarrays. This is what “DSA shows up in real systems” looks like.
Simple implementation with Bucket4j:
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
private Bucket createBucket() {
return Bucket.builder()
.addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
.build();
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
String clientId = request.getRemoteAddr(); // Or use authenticated userId
Bucket bucket = buckets.computeIfAbsent(clientId, k -> createBucket());
if (bucket.tryConsume(1)) {
chain.doFilter(request, response);
} else {
response.setStatus(429); // Too Many Requests
response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
}
}
}
Day 3: Structured Logging — Debug Production Like a Pro
Bad logging:
System.out.println("Error occurred"); // Useless
logger.info("Processing request"); // What request? For whom?
Good logging:
logger.info("Creating expense: groupId={}, amount={}, paidBy={}",
groupId, request.amount(), request.paidByUserId());
logger.error("Expense creation failed: groupId={}, reason={}",
groupId, ex.getMessage(), ex);
Add MDC (Mapped Diagnostic Context) for request tracing:
@Component
public class RequestIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
String requestId = UUID.randomUUID().toString().substring(0, 8);
MDC.put("requestId", requestId);
response.setHeader("X-Request-Id", requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
Now every log line includes the request ID. When a user reports “my payment failed,” you can trace the entire request through your logs.
Day 4: Health Checks + Actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
Custom health check:
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
jdbcTemplate.queryForObject("SELECT 1", Integer.class);
return Health.up().withDetail("database", "PostgreSQL responding").build();
} catch (Exception e) {
return Health.down().withException(e).build();
}
}
}
GET /actuator/health → tells you if your app and all its dependencies are alive.
Every production system needs this. Load balancers use it to decide whether to route traffic to your server.
Day 5: Streams & Functional Java — Clean Code
Refactor your service layer to use Java streams where appropriate:
// Calculate who owes whom in a group
public List<BalanceDTO> calculateBalances(Long groupId) {
List<Expense> expenses = expenseRepo.findByGroupId(groupId);
Map<Long, BigDecimal> netBalances = expenses.stream()
.flatMap(expense -> {
Stream<Map.Entry<Long, BigDecimal>> paid = Stream.of(
Map.entry(expense.getPaidBy().getId(), expense.getAmount()));
Stream<Map.Entry<Long, BigDecimal>> owed = expense.getSplits().stream()
.map(split -> Map.entry(split.getUser().getId(), split.getAmount().negate()));
return Stream.concat(paid, owed);
})
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.reducing(BigDecimal.ZERO, Map.Entry::getValue, BigDecimal::add)
));
return netBalances.entrySet().stream()
.map(e -> new BalanceDTO(e.getKey(), e.getValue()))
.sorted(Comparator.comparing(BalanceDTO::amount).reversed())
.toList();
}
Weekend: Month 2 Checkpoint
Your project should now have:
- JWT authentication (register, login, protected endpoints)
- Authorization (users can only access their own data)
- PostgreSQL with proper indexes (verified with EXPLAIN ANALYZE)
- Redis caching on balance calculations
- Pagination on list endpoints
- Rate limiting
- Structured logging with request IDs
- Health check endpoint
- DSA modules 01–05 done, DSA 06 — Binary Search this weekend
- 30+ DSA problems solved
- Can whiteboard a basic system design
This is a production-grade backend. Most mid-level engineers don’t have all of this in their personal projects.
Resources
| What | Where | Time |
|---|---|---|
| Spring Pagination | Baeldung “Spring Data Pagination” | 15 min |
| Rate limiting | Baeldung “Rate Limiting with Bucket4j” | 20 min |
| Structured logging | Baeldung “SLF4J MDC” | 15 min |
| Spring Actuator | Spring docs “Production-ready Features” | 20 min |