Advanced · Architect

Design a Production-Grade Capstone

Bring contracts, consistency, security, observability, testing, resilience, and delivery into one reviewable slice.

Primary source: Spring Framework 6.2 Core Technologies: The IoC Container

60 min

Lesson 3 of 3

1d streak

The Architectural Question

Portfolio pieces showcase happy paths; senior reviews probe failure drills, decision rationale, and whether another engineer can reproduce the evidence.

What this guide proves

  • Document decisions and rejected alternatives
  • Exercise failure and recovery paths
  • Ship evidence that another engineer can reproduce

The Enterprise Capstone: Production-Grade Hardened Service

This capstone synthesizes every architectural discipline learned across the curriculum into a single, hardened enterprise service. A production microservice is not just controllers and entities; it is a resilient distributed system component.

The unified architecture incorporates:

1. Zero-Trust Security: Stateless JWT token authentication with role-based method security and strict CORS configuration.

2. Hexagonal Domain Isolation: Application-owned ports decoupling pure domain models from Postgres persistence and external REST APIs.

3. Fail-Open Distributed Caching: Redis cache-aside with Jackson 2 JSON serialization, TTL jittering, and custom CacheErrorHandler.

4. Resilient HTTP Integrations: Pooled RestClient instances wrapped with Resilience4j circuit breakers and exponential backoff retries.

5. Full Observability: Micrometer metrics, OpenTelemetry distributed tracing spans, and Kubernetes readiness/liveness state probes.

Distributed Consistency: The Transactional Outbox Pattern

In a microservices architecture, a single business action often requires updating a local relational database AND publishing an event to a message broker (e.g. Kafka or RabbitMQ). Dual-writing directly to both systems is fundamentally flawed: if the broker publish fails or times out after the database commits, downstream services fall out of sync permanently.

The Transactional Outbox pattern guarantees at-least-once event delivery without two-phase distributed transactions (XA):

src/main/java/com/example/outbox/TransactionalOutboxRepository.java
package com.example.outbox;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.UUID;

@Repository
public class TransactionalOutboxRepository {

    private final JdbcClient jdbcClient;
    private final ObjectMapper objectMapper;

    public TransactionalOutboxRepository(JdbcClient jdbcClient, ObjectMapper objectMapper) {
        this.jdbcClient = jdbcClient;
        this.objectMapper = objectMapper;
    }

    // Must participate in the SAME local database transaction as the business entity update!
    @Transactional
    public void publishEvent(String aggregateType, String aggregateId, String eventType, Object payload) {
        try {
            String json = objectMapper.writeValueAsString(payload);
            jdbcClient.sql("""
                INSERT INTO outbox_events (id, aggregate_type, aggregate_id, event_type, payload, created_at, processed)
                VALUES (:id, :aggregateType, :aggregateId, :eventType, :payload::jsonb, :createdAt, false)
                """)
                .param("id", UUID.randomUUID())
                .param("aggregateType", aggregateType)
                .param("aggregateId", aggregateId)
                .param("eventType", eventType)
                .param("payload", json)
                .param("createdAt", Instant.now())
                .update();
        } catch (Exception ex) {
            throw new RuntimeException("Failed to serialize outbox event", ex);
        }
    }
}

The Production Verification Checklist & SLO Validation

Before promoting any microservice to production, execute this comprehensive operational readiness verification:

Verification AreaTarget Production SLAVerification Method
p99 Latency< 50ms for cached reads, < 150ms for writesLoad test with hey at 1000 requests/second
Graceful Shutdown0 dropped in-flight requests during pod scale-downTrigger rolling restart during live traffic test
Fail-Open CachingHTTP 200 OK maintained during Redis outagePause Redis container during simulated load
Secret Sanitization0 plain-text passwords in logs, dumps, or envActuator /env endpoint audit
Zero-Trust Security401/403 returned on forged JWT or missing claimsAutomated security integration test suite

End-to-End Capstone Verification Walkthrough

Execute the complete end-to-end verification script to prove all architectural guarantees hold simultaneously:

snippet.bash
# 1. Boot up application stack via docker-compose
docker compose up -d postgres redis

# 2. Run database migration and test suite
./mvnw clean verify

# 3. Verify health probes
curl -s http://localhost:8080/actuator/health | jq .status
# EXPECTED: "UP"

# 4. Create and process order through hexagonal use case
curl -i -X POST http://localhost:8080/api/orders \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"CUST-101","items":[{"sku":"SKU-1","quantity":2}]}'
# HTTP/1.1 201 Created

# 5. Verify atomic outbox event persisted in PostgreSQL
psql -d orders_db -c "SELECT aggregate_id, event_type, processed FROM outbox_events;"
# aggregate_id | event_type  | processed
# -------------+-------------+-----------
# ORD-1001     | ORDER_PLACED| f

Active Recall Checkpoint

Why does the Transactional Outbox pattern eliminate dual-write distributed transaction bugs?

Hint: Consider what happens when a database commit succeeds but the message broker network connection drops.

Show answer & explanation

By storing the outbound event in an outbox table inside the same local relational transaction as the business entity, either both the entity and the event save atomically, or both roll back together.

Working Implementation

Code Walkthrough

Present the checkout slice with an ADR set, runbook, threat model, and live failure drill.

ProductionMasteryCapstoneLab.java
package com.example.capstone;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.UUID;

@Service
public class ProductionMasteryCapstoneLab {

    private final JdbcClient jdbcClient;

    public ProductionMasteryCapstoneLab(JdbcClient jdbcClient) {
        this.jdbcClient = jdbcClient;
    }

    public record OrderRecord(String orderId, String customerId, int totalCents, String status) {}

    // Atomic outbox write participating in local relational transaction
    @Transactional
    public OrderRecord createOrderWithOutbox(String customerId, int totalCents) {
        String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8);
        var order = new OrderRecord(orderId, customerId, totalCents, "CREATED");

        // 1. Insert order record
        jdbcClient.sql("INSERT INTO orders (id, customer_id, total_cents, status) VALUES (?, ?, ?, ?)")
            .params(order.orderId(), order.customerId(), order.totalCents(), order.status())
            .update();

        // 2. Insert transactional outbox event atomically in same transaction
        jdbcClient.sql("""
            INSERT INTO outbox_events (id, aggregate_type, aggregate_id, event_type, payload, created_at)
            VALUES (?, 'ORDER', ?, 'ORDER_CREATED', ?::jsonb, ?)
            """)
            .params(UUID.randomUUID(), orderId, "{\"totalCents\":" + totalCents + "}", Instant.now())
            .update();

        return order;
    }

    @Cacheable(cacheNames = "orders", key = "#orderId")
    public OrderRecord findCachedOrder(String orderId) {
        return jdbcClient.sql("SELECT id, customer_id, total_cents, status FROM orders WHERE id = ?")
            .param(orderId)
            .query((rs, rowNum) -> new OrderRecord(
                rs.getString("id"),
                rs.getString("customer_id"),
                rs.getInt("total_cents"),
                rs.getString("status")
            ))
            .single();
    }
}
Transactional Outbox guarantees zero-data-loss event publishing between PostgreSQL and message brokers.
Combines fail-open distributed caching, transactional isolation, and strict domain invariants.
Hardened against network partitions, broker outages, and concurrent race conditions.

Synthesis & Production Verification

Deliberate Failure Condition

  1. Break: Ship a capstone without ADRs or a failure drill
  2. Observe: The defense stalls on "why did you choose this" and every recovery claim stays unverified
  3. Repair: Decision records, a runbook, a threat model, and a live drill are what turn a demo into defensible engineering evidence.

Production Acceptance Criteria

  • Document decisions with rejected alternatives in an ADR set
  • Exercise failure and recovery paths live, not on slides
  • Package evidence another engineer can reproduce from scratch

Architectural & Technical Review

Core Interview Questions

Q01How does the Transactional Outbox pattern resolve the distributed dual-write consistency problem?Show answer

By storing the outbound domain event in an outbox table within the same local database transaction as the business entity, either both save atomically or both roll back, eliminating dual-write race conditions.

Q02Which core pillars constitute a production-hardened Spring Boot microservice architecture?Show answer

Zero-trust security (stateless JWTs), hexagonal domain isolation, cache-aside caching with TTL jitter and bounded fallbacks, pooled resilient HTTP clients with circuit breakers, and comprehensive OpenTelemetry tracing.

Q03What golden verification criteria must pass before promoting an enterprise service to production?Show answer

P99 latency SLO (< 50ms cached, < 150ms write), graceful shutdown with zero dropped requests under rolling restart, fail-open behavior during Redis outage, and automated ArchUnit architecture compliance.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Publishing directly to message brokers inside a database transaction without an outbox table — Network drops or broker timeouts after database commit lead to lost events and permanent desynchronization.
  • Pitfall: Neglecting fail-open caching and timeout budgets in inter-service microservice calls — Allows an upstream outage to cascade across the entire microservice graph.

Module Certification Quiz

Prove Module Mastery

Comprehensive question set covering all lessons in this module.

Take Module Quiz

Primary Documentation & Specifications