{"skill":{"slug":"afrexai-spring-boot-production","displayName":"Spring Boot Production Engineering","summary":"Implement production-ready Spring Boot apps with best practices for architecture, security, observability, testing, deployment, and performance optimization.","description":"# Spring Boot Production Engineering\n\n> Complete production engineering methodology for Spring Boot & Java/Kotlin applications — architecture, security, observability, testing, deployment, and performance optimization.\n\n## Quick Health Check\n\nScore your Spring Boot application (1 = needs work, 2 = acceptable):\n\n| Signal | Check | /2 |\n|--------|-------|----|\n| 🏗️ Architecture | Clean layered architecture with dependency injection? | |\n| 🔒 Security | Spring Security configured with proper auth + CORS + CSRF? | |\n| 📊 Observability | Structured logging + metrics + health endpoints? | |\n| 🧪 Testing | Unit + integration + slice tests with >70% coverage? | |\n| ⚡ Performance | Connection pooling + caching + async where appropriate? | |\n| 🚀 Deployment | Containerized with CI/CD + zero-downtime deploys? | |\n| 📝 API Design | OpenAPI docs + versioning + consistent error responses? | |\n| 🛡️ Resilience | Circuit breakers + retries + graceful degradation? | |\n\n**Score: /16** → ≤8 Critical · 9-12 Improving · 13-14 Good · 15-16 Production-ready\n\n---\n\n## Phase 1: Project Architecture\n\n### Recommended Project Structure\n\n```\nsrc/main/java/com/example/app/\n├── Application.java                 # @SpringBootApplication entry\n├── config/                          # Configuration classes\n│   ├── SecurityConfig.java\n│   ├── WebConfig.java\n│   ├── CacheConfig.java\n│   └── AsyncConfig.java\n├── domain/                          # Domain models & business logic\n│   ├── model/                       # JPA entities / domain objects\n│   ├── repository/                  # Spring Data repositories\n│   ├── service/                     # Business logic services\n│   └── event/                       # Domain events\n├── api/                             # REST controllers\n│   ├── controller/                  # @RestController classes\n│   ├── dto/                         # Request/Response DTOs\n│   ├── mapper/                      # Entity ↔ DTO mappers\n│   └── exception/                   # API exception handlers\n├── infrastructure/                  # External integrations\n│   ├── client/                      # REST/gRPC clients\n│   ├── messaging/                   # Kafka/RabbitMQ producers/consumers\n│   └── storage/                     # S3/file storage\n└── common/                          # Shared utilities\n    ├── exception/                   # Base exceptions\n    ├── validation/                  # Custom validators\n    └── util/                        # Helpers\n```\n\n### 7 Architecture Rules\n\n1. **Controllers are thin** — validate input, call service, return DTO. No business logic.\n2. **Services own business logic** — transaction boundaries live here.\n3. **Repositories are interfaces** — Spring Data generates implementations.\n4. **DTOs at boundaries** — never expose JPA entities in API responses.\n5. **Constructor injection only** — no `@Autowired` on fields (testability).\n6. **Package by feature for large apps** — when >20 services, switch from layer-based to feature-based.\n7. **No circular dependencies** — if A depends on B depends on A, extract shared logic to C.\n\n### Spring Boot Starter Selection\n\n```yaml\n# build.gradle.kts (recommended over Maven for Kotlin DSL + type safety)\ndependencies:\n  # Core\n  - spring-boot-starter-web          # REST APIs (embedded Tomcat)\n  - spring-boot-starter-webflux      # Reactive APIs (Netty) — choose ONE\n  - spring-boot-starter-validation   # Bean Validation (Jakarta)\n  \n  # Data\n  - spring-boot-starter-data-jpa     # JPA + Hibernate\n  - spring-boot-starter-data-redis   # Redis caching\n  \n  # Security\n  - spring-boot-starter-security     # Spring Security\n  - spring-boot-starter-oauth2-resource-server  # JWT validation\n  \n  # Observability\n  - spring-boot-starter-actuator     # Health, metrics, info\n  - micrometer-registry-prometheus   # Prometheus metrics export\n  \n  # Resilience\n  - resilience4j-spring-boot3        # Circuit breaker, retry, rate limit\n  \n  # Testing\n  - spring-boot-starter-test         # JUnit 5 + Mockito + AssertJ\n  - spring-boot-testcontainers       # Real DB/Redis in tests\n```\n\n### Framework Decision: Spring Boot vs Alternatives\n\n| Factor | Spring Boot | Quarkus | Micronaut | Ktor (Kotlin) |\n|--------|------------|---------|-----------|---------------|\n| Startup time | 2-5s | 0.5-1s | 1-2s | 1-2s |\n| Memory | 200-400MB | 50-150MB | 100-200MB | 80-150MB |\n| Ecosystem | ★★★★★ | ★★★☆☆ | ★★★☆☆ | ★★☆☆☆ |\n| Enterprise adoption | Dominant | Growing | Niche | Niche |\n| Native compilation | GraalVM (complex) | Native (easy) | Native (easy) | GraalVM |\n| Team hiring | Easy | Hard | Hard | Hard |\n\n**Decision rule**: Spring Boot unless startup time <1s is critical (serverless/CLI) → Quarkus.\n\n---\n\n## Phase 2: Configuration & Profiles\n\n### application.yml Production Template\n\n```yaml\nspring:\n  application:\n    name: ${APP_NAME:my-service}\n  profiles:\n    active: ${SPRING_PROFILES_ACTIVE:local}\n  \n  # Database\n  datasource:\n    url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/mydb}\n    username: ${DATABASE_USERNAME:postgres}\n    password: ${DATABASE_PASSWORD:postgres}\n    hikari:\n      maximum-pool-size: ${DB_POOL_SIZE:10}\n      minimum-idle: ${DB_POOL_MIN:5}\n      connection-timeout: 3000\n      idle-timeout: 600000\n      max-lifetime: 1800000\n      leak-detection-threshold: 60000\n  \n  jpa:\n    open-in-view: false  # CRITICAL — disable OSIV anti-pattern\n    hibernate:\n      ddl-auto: validate  # Production: NEVER use update/create\n    properties:\n      hibernate:\n        default_batch_fetch_size: 25\n        order_inserts: true\n        order_updates: true\n        jdbc:\n          batch_size: 50\n          batch_versioned_data: true\n  \n  # Jackson\n  jackson:\n    default-property-inclusion: non_null\n    serialization:\n      write-dates-as-timestamps: false\n    deserialization:\n      fail-on-unknown-properties: false\n  \n  # Cache\n  cache:\n    type: redis\n    redis:\n      time-to-live: 3600000  # 1 hour default\n\nserver:\n  port: ${SERVER_PORT:8080}\n  shutdown: graceful  # Wait for active requests\n  tomcat:\n    max-threads: ${TOMCAT_MAX_THREADS:200}\n    accept-count: 100\n    connection-timeout: 5000\n\nmanagement:\n  endpoints:\n    web:\n      exposure:\n        include: health,info,prometheus,metrics\n  endpoint:\n    health:\n      show-details: when-authorized\n      probes:\n        enabled: true  # Kubernetes liveness/readiness\n  metrics:\n    tags:\n      application: ${spring.application.name}\n\n# Graceful shutdown\nspring.lifecycle.timeout-per-shutdown-phase: 30s\n```\n\n### Profile Strategy\n\n| Profile | Purpose | Config |\n|---------|---------|--------|\n| `local` | Development | H2/local Postgres, debug logging |\n| `test` | Testing | Testcontainers, no external deps |\n| `staging` | Pre-production | Real deps, reduced resources |\n| `production` | Live | Full resources, minimal logging |\n\n### Configuration Rules\n\n1. **Never hardcode secrets** — always use environment variables or vault\n2. **Disable `open-in-view`** — prevents lazy loading in controller layer (performance killer)\n3. **Set `ddl-auto: validate`** in production — use Flyway/Liquibase for migrations\n4. **Configure HikariCP explicitly** — defaults are often wrong for production\n5. **Enable graceful shutdown** — `server.shutdown: graceful` + timeout\n\n---\n\n## Phase 3: JPA & Database Patterns\n\n### Entity Design\n\n```java\n@MappedSuperclass\npublic abstract class BaseEntity {\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Long id;\n    \n    @CreationTimestamp\n    @Column(updatable = false)\n    private Instant createdAt;\n    \n    @UpdateTimestamp\n    private Instant updatedAt;\n    \n    @Version  // Optimistic locking\n    private Long version;\n}\n\n@Entity\n@Table(name = \"users\", indexes = {\n    @Index(name = \"idx_users_email\", columnList = \"email\", unique = true),\n    @Index(name = \"idx_users_status\", columnList = \"status\")\n})\npublic class User extends BaseEntity {\n    \n    @Column(nullable = false, length = 255)\n    private String email;\n    \n    @Enumerated(EnumType.STRING)\n    @Column(nullable = false, length = 20)\n    private UserStatus status;\n    \n    @OneToMany(mappedBy = \"user\", fetch = FetchType.LAZY)  // ALWAYS lazy\n    private List<Order> orders = new ArrayList<>();\n}\n```\n\n### N+1 Prevention\n\n```java\n// ❌ N+1 problem — loads each user's orders individually\nList<User> users = userRepository.findAll();\nusers.forEach(u -> u.getOrders().size());  // N additional queries\n\n// ✅ JOIN FETCH — single query\n@Query(\"SELECT u FROM User u JOIN FETCH u.orders WHERE u.status = :status\")\nList<User> findByStatusWithOrders(@Param(\"status\") UserStatus status);\n\n// ✅ EntityGraph — declarative\n@EntityGraph(attributePaths = {\"orders\", \"orders.items\"})\nList<User> findByStatus(UserStatus status);\n\n// ✅ Batch fetching (configured globally)\n# application.yml: hibernate.default_batch_fetch_size: 25\n```\n\n### Repository Patterns\n\n```java\npublic interface UserRepository extends JpaRepository<User, Long> {\n    \n    // Derived queries — simple cases only\n    Optional<User> findByEmail(String email);\n    boolean existsByEmail(String email);\n    \n    // Projections — return only needed fields\n    @Query(\"SELECT new com.example.dto.UserSummary(u.id, u.email, u.status) \" +\n           \"FROM User u WHERE u.status = :status\")\n    List<UserSummary> findSummariesByStatus(@Param(\"status\") UserStatus status);\n    \n    // Pagination\n    Page<User> findByStatus(UserStatus status, Pageable pageable);\n    \n    // Bulk operations — bypass Hibernate cache\n    @Modifying(clearAutomatically = true)\n    @Query(\"UPDATE User u SET u.status = :status WHERE u.lastLoginAt < :threshold\")\n    int deactivateInactiveUsers(@Param(\"status\") UserStatus status,\n                                @Param(\"threshold\") Instant threshold);\n}\n```\n\n### Migration with Flyway\n\n```sql\n-- V1__create_users_table.sql\nCREATE TABLE users (\n    id          BIGSERIAL PRIMARY KEY,\n    email       VARCHAR(255) NOT NULL,\n    status      VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',\n    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    version     BIGINT NOT NULL DEFAULT 0,\n    CONSTRAINT uk_users_email UNIQUE (email)\n);\n\nCREATE INDEX idx_users_status ON users(status);\n```\n\n### 8 JPA Rules\n\n1. **Always use `FetchType.LAZY`** — eager loading causes N+1\n2. **Use `@Version` for optimistic locking** — prevents lost updates\n3. **Prefer projections over full entities** — `SELECT new DTO(...)` for read-only\n4. **Batch inserts/updates** — configure `batch_size` + `order_inserts`\n5. **Never use `ddl-auto: update` in production** — Flyway/Liquibase only\n6. **Use `@NaturalId` for business keys** — email, ISBN, etc.\n7. **Avoid bidirectional mappings unless needed** — more complexity, more bugs\n8. **Test queries with real database** — Testcontainers, not H2\n\n---\n\n## Phase 4: REST API Design\n\n### Controller Pattern\n\n```java\n@RestController\n@RequestMapping(\"/api/v1/users\")\n@RequiredArgsConstructor\n@Validated\npublic class UserController {\n    \n    private final UserService userService;\n    private final UserMapper userMapper;\n    \n    @GetMapping\n    public Page<UserResponse> listUsers(\n            @RequestParam(defaultValue = \"0\") int page,\n            @RequestParam(defaultValue = \"20\") int size,\n            @RequestParam(required = false) UserStatus status) {\n        \n        Pageable pageable = PageRequest.of(page, size, Sort.by(\"createdAt\").descending());\n        return userService.findUsers(status, pageable)\n                .map(userMapper::toResponse);\n    }\n    \n    @GetMapping(\"/{id}\")\n    public UserResponse getUser(@PathVariable Long id) {\n        return userMapper.toResponse(userService.findById(id));\n    }\n    \n    @PostMapping\n    @ResponseStatus(HttpStatus.CREATED)\n    public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {\n        User user = userService.create(request);\n        return userMapper.toResponse(user);\n    }\n    \n    @PutMapping(\"/{id}\")\n    public UserResponse updateUser(@PathVariable Long id,\n                                    @Valid @RequestBody UpdateUserRequest request) {\n        User user = userService.update(id, request);\n        return userMapper.toResponse(user);\n    }\n    \n    @DeleteMapping(\"/{id}\")\n    @ResponseStatus(HttpStatus.NO_CONTENT)\n    public void deleteUser(@PathVariable Long id) {\n        userService.delete(id);\n    }\n}\n```\n\n### DTO Validation\n\n```java\npublic record CreateUserRequest(\n    @NotBlank @Email @Size(max = 255)\n    String email,\n    \n    @NotBlank @Size(min = 2, max = 100)\n    String name,\n    \n    @NotNull\n    UserRole role\n) {}\n\npublic record UserResponse(\n    Long id,\n    String email,\n    String name,\n    UserStatus status,\n    Instant createdAt\n) {}\n```\n\n### Global Error Handling\n\n```java\n@RestControllerAdvice\n@Slf4j\npublic class GlobalExceptionHandler {\n    \n    @ExceptionHandler(EntityNotFoundException.class)\n    @ResponseStatus(HttpStatus.NOT_FOUND)\n    public ErrorResponse handleNotFound(EntityNotFoundException ex) {\n        return new ErrorResponse(\"NOT_FOUND\", ex.getMessage());\n    }\n    \n    @ExceptionHandler(MethodArgumentNotValidException.class)\n    @ResponseStatus(HttpStatus.BAD_REQUEST)\n    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {\n        Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()\n            .collect(Collectors.toMap(\n                FieldError::getField,\n                fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : \"invalid\",\n                (a, b) -> a\n            ));\n        return new ErrorResponse(\"VALIDATION_ERROR\", \"Invalid request\", errors);\n    }\n    \n    @ExceptionHandler(DataIntegrityViolationException.class)\n    @ResponseStatus(HttpStatus.CONFLICT)\n    public ErrorResponse handleConflict(DataIntegrityViolationException ex) {\n        return new ErrorResponse(\"CONFLICT\", \"Resource already exists\");\n    }\n    \n    @ExceptionHandler(Exception.class)\n    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)\n    public ErrorResponse handleUnexpected(Exception ex) {\n        log.error(\"Unexpected error\", ex);\n        return new ErrorResponse(\"INTERNAL_ERROR\", \"An unexpected error occurred\");\n    }\n}\n\npublic record ErrorResponse(\n    String code,\n    String message,\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    Map<String, String> details\n) {\n    public ErrorResponse(String code, String message) {\n        this(code, message, null);\n    }\n}\n```\n\n---\n\n## Phase 5: Security\n\n### Spring Security 6 Configuration\n\n```java\n@Configuration\n@EnableWebSecurity\n@EnableMethodSecurity\n@RequiredArgsConstructor\npublic class SecurityConfig {\n    \n    @Bean\n    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {\n        return http\n            .csrf(csrf -> csrf.disable())  // Disable for stateless APIs\n            .cors(cors -> cors.configurationSource(corsConfig()))\n            .sessionManagement(session -> \n                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))\n            .authorizeHttpRequests(auth -> auth\n                .requestMatchers(\"/api/v1/auth/**\").permitAll()\n                .requestMatchers(\"/actuator/health/**\").permitAll()\n                .requestMatchers(\"/api/v1/admin/**\").hasRole(\"ADMIN\")\n                .anyRequest().authenticated()\n            )\n            .oauth2ResourceServer(oauth2 -> oauth2\n                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))\n            )\n            .exceptionHandling(ex -> ex\n                .authenticationEntryPoint((req, res, e) -> {\n                    res.setStatus(401);\n                    res.getWriter().write(\"{\\\"code\\\":\\\"UNAUTHORIZED\\\",\\\"message\\\":\\\"Invalid or missing token\\\"}\");\n                })\n            )\n            .headers(headers -> headers\n                .contentSecurityPolicy(csp -> csp.policyDirectives(\"default-src 'self'\"))\n                .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)\n            )\n            .build();\n    }\n    \n    private CorsConfigurationSource corsConfig() {\n        CorsConfiguration config = new CorsConfiguration();\n        config.setAllowedOrigins(List.of(\"https://app.example.com\"));\n        config.setAllowedMethods(List.of(\"GET\", \"POST\", \"PUT\", \"DELETE\"));\n        config.setAllowedHeaders(List.of(\"Authorization\", \"Content-Type\"));\n        config.setMaxAge(3600L);\n        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\n        source.registerCorsConfiguration(\"/api/**\", config);\n        return source;\n    }\n    \n    private JwtAuthenticationConverter jwtConverter() {\n        JwtGrantedAuthoritiesConverter authorities = new JwtGrantedAuthoritiesConverter();\n        authorities.setAuthorityPrefix(\"ROLE_\");\n        authorities.setAuthoritiesClaimName(\"roles\");\n        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();\n        converter.setJwtGrantedAuthoritiesConverter(authorities);\n        return converter;\n    }\n}\n```\n\n### 10-Point Security Checklist\n\n| # | Check | Priority |\n|---|-------|----------|\n| 1 | CSRF disabled for stateless APIs, enabled for session-based | P0 |\n| 2 | CORS configured with specific origins (no wildcards in prod) | P0 |\n| 3 | JWT validation with proper issuer/audience checks | P0 |\n| 4 | Input validation on all request DTOs (`@Valid`) | P0 |\n| 5 | SQL injection prevention (parameterized queries only) | P0 |\n| 6 | Secrets in environment variables or vault (never in code) | P0 |\n| 7 | Security headers (CSP, X-Frame-Options, HSTS) | P1 |\n| 8 | Rate limiting on auth endpoints | P1 |\n| 9 | Dependency vulnerability scanning (OWASP, Snyk) | P1 |\n| 10 | Method-level security (`@PreAuthorize`) for sensitive operations | P1 |\n\n---\n\n## Phase 6: Service Layer & Business Logic\n\n### Service Pattern\n\n```java\n@Service\n@RequiredArgsConstructor\n@Transactional(readOnly = true)  // Default read-only\n@Slf4j\npublic class UserService {\n    \n    private final UserRepository userRepository;\n    private final PasswordEncoder passwordEncoder;\n    private final ApplicationEventPublisher eventPublisher;\n    \n    public User findById(Long id) {\n        return userRepository.findById(id)\n            .orElseThrow(() -> new EntityNotFoundException(\"User not found: \" + id));\n    }\n    \n    public Page<User> findUsers(UserStatus status, Pageable pageable) {\n        if (status != null) {\n            return userRepository.findByStatus(status, pageable);\n        }\n        return userRepository.findAll(pageable);\n    }\n    \n    @Transactional  // Write transaction\n    public User create(CreateUserRequest request) {\n        if (userRepository.existsByEmail(request.email())) {\n            throw new ConflictException(\"Email already registered: \" + request.email());\n        }\n        \n        User user = User.builder()\n            .email(request.email())\n            .name(request.name())\n            .status(UserStatus.ACTIVE)\n            .build();\n        \n        user = userRepository.save(user);\n        \n        eventPublisher.publishEvent(new UserCreatedEvent(user.getId(), user.getEmail()));\n        log.info(\"User created: id={}, email={}\", user.getId(), user.getEmail());\n        \n        return user;\n    }\n    \n    @Transactional\n    @CacheEvict(value = \"users\", key = \"#id\")\n    public User update(Long id, UpdateUserRequest request) {\n        User user = findById(id);\n        // Update fields...\n        return userRepository.save(user);\n    }\n}\n```\n\n### Domain Events\n\n```java\npublic record UserCreatedEvent(Long userId, String email) {}\n\n@Component\n@RequiredArgsConstructor\n@Slf4j\npublic class UserEventListener {\n    \n    private final EmailService emailService;\n    \n    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)\n    @Async\n    public void onUserCreated(UserCreatedEvent event) {\n        log.info(\"Sending welcome email to user: {}\", event.userId());\n        emailService.sendWelcome(event.email());\n    }\n}\n```\n\n---\n\n## Phase 7: Caching\n\n### Redis Cache Configuration\n\n```java\n@Configuration\n@EnableCaching\npublic class CacheConfig {\n    \n    @Bean\n    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {\n        RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()\n            .entryTtl(Duration.ofHours(1))\n            .serializeValuesWith(\n                RedisSerializationContext.SerializationPair.fromSerializer(\n                    new GenericJackson2JsonRedisSerializer()\n                ))\n            .disableCachingNullValues();\n        \n        Map<String, RedisCacheConfiguration> configs = Map.of(\n            \"users\", defaults.entryTtl(Duration.ofMinutes(30)),\n            \"products\", defaults.entryTtl(Duration.ofHours(2)),\n            \"config\", defaults.entryTtl(Duration.ofHours(24))\n        );\n        \n        return RedisCacheManager.builder(factory)\n            .cacheDefaults(defaults)\n            .withInitialCacheConfigurations(configs)\n            .build();\n    }\n}\n```\n\n### Cache Usage\n\n```java\n@Cacheable(value = \"users\", key = \"#id\")\npublic UserResponse getUserById(Long id) { ... }\n\n@CachePut(value = \"users\", key = \"#result.id\")\npublic UserResponse updateUser(Long id, UpdateUserRequest req) { ... }\n\n@CacheEvict(value = \"users\", key = \"#id\")\npublic void deleteUser(Long id) { ... }\n\n@CacheEvict(value = \"users\", allEntries = true)\n@Scheduled(fixedRate = 3600000)  // Hourly full invalidation\npublic void evictAllUsers() { ... }\n```\n\n---\n\n## Phase 8: Resilience\n\n### Resilience4j Configuration\n\n```yaml\nresilience4j:\n  circuitbreaker:\n    instances:\n      payment-service:\n        sliding-window-size: 10\n        failure-rate-threshold: 50\n        wait-duration-in-open-state: 10s\n        permitted-number-of-calls-in-half-open-state: 3\n        slow-call-duration-threshold: 2s\n        slow-call-rate-threshold: 80\n  \n  retry:\n    instances:\n      payment-service:\n        max-attempts: 3\n        wait-duration: 500ms\n        exponential-backoff-multiplier: 2\n        retry-exceptions:\n          - java.io.IOException\n          - java.util.concurrent.TimeoutException\n        ignore-exceptions:\n          - com.example.exception.BusinessException\n  \n  ratelimiter:\n    instances:\n      api:\n        limit-for-period: 100\n        limit-refresh-period: 1s\n        timeout-duration: 0s\n```\n\n### Usage\n\n```java\n@CircuitBreaker(name = \"payment-service\", fallbackMethod = \"paymentFallback\")\n@Retry(name = \"payment-service\")\npublic PaymentResponse processPayment(PaymentRequest request) {\n    return paymentClient.charge(request);\n}\n\nprivate PaymentResponse paymentFallback(PaymentRequest request, Throwable t) {\n    log.warn(\"Payment service unavailable, queuing for retry: {}\", t.getMessage());\n    paymentQueue.enqueue(request);\n    return PaymentResponse.pending();\n}\n```\n\n---\n\n## Phase 9: Observability\n\n### Structured Logging\n\n```java\n// logback-spring.xml\n// Use JSON format in production\n@Slf4j\npublic class OrderService {\n    \n    public Order processOrder(CreateOrderRequest request) {\n        try (var mdc = MDC.putCloseable(\"orderId\", request.orderId());\n             var userMdc = MDC.putCloseable(\"userId\", request.userId())) {\n            \n            log.info(\"Processing order: items={}, total={}\", \n                     request.items().size(), request.total());\n            // All logs within this scope include orderId + userId\n        }\n    }\n}\n```\n\n### Metrics with Micrometer\n\n```java\n@Component\n@RequiredArgsConstructor\npublic class OrderMetrics {\n    \n    private final MeterRegistry registry;\n    \n    public void recordOrderProcessed(String status, Duration duration) {\n        registry.counter(\"orders.processed\", \"status\", status).increment();\n        registry.timer(\"orders.processing.time\", \"status\", status)\n                .record(duration);\n    }\n    \n    public void recordActiveOrders(int count) {\n        registry.gauge(\"orders.active\", count);\n    }\n}\n```\n\n### Health Indicators\n\n```java\n@Component\npublic class PaymentServiceHealthIndicator implements HealthIndicator {\n    \n    private final PaymentClient paymentClient;\n    \n    @Override\n    public Health health() {\n        try {\n            paymentClient.ping();\n            return Health.up().withDetail(\"latency\", \"ok\").build();\n        } catch (Exception e) {\n            return Health.down().withException(e).build();\n        }\n    }\n}\n```\n\n---\n\n## Phase 10: Testing\n\n### Test Pyramid\n\n| Level | What | Tools | Coverage Target |\n|-------|------|-------|----------------|\n| Unit | Services, mappers, utils | JUnit 5 + Mockito | 80% |\n| Slice | Controllers, repositories | @WebMvcTest, @DataJpaTest | Key paths |\n| Integration | Full flow with real DB | @SpringBootTest + Testcontainers | Happy + error |\n| Contract | API contracts | Spring Cloud Contract / Pact | All endpoints |\n\n### Unit Test Pattern\n\n```java\n@ExtendWith(MockitoExtension.class)\nclass UserServiceTest {\n    \n    @Mock UserRepository userRepository;\n    @Mock ApplicationEventPublisher eventPublisher;\n    @InjectMocks UserService userService;\n    \n    @Test\n    void create_validRequest_savesAndPublishesEvent() {\n        var request = new CreateUserRequest(\"test@example.com\", \"Test User\", UserRole.USER);\n        var savedUser = User.builder().id(1L).email(request.email()).build();\n        \n        when(userRepository.existsByEmail(request.email())).thenReturn(false);\n        when(userRepository.save(any(User.class))).thenReturn(savedUser);\n        \n        User result = userService.create(request);\n        \n        assertThat(result.getId()).isEqualTo(1L);\n        verify(eventPublisher).publishEvent(any(UserCreatedEvent.class));\n    }\n    \n    @Test\n    void create_duplicateEmail_throwsConflict() {\n        var request = new CreateUserRequest(\"existing@example.com\", \"Test\", UserRole.USER);\n        when(userRepository.existsByEmail(request.email())).thenReturn(true);\n        \n        assertThatThrownBy(() -> userService.create(request))\n            .isInstanceOf(ConflictException.class)\n            .hasMessageContaining(\"already registered\");\n    }\n}\n```\n\n### Controller Slice Test\n\n```java\n@WebMvcTest(UserController.class)\n@Import(SecurityConfig.class)\nclass UserControllerTest {\n    \n    @Autowired MockMvc mockMvc;\n    @MockBean UserService userService;\n    @MockBean UserMapper userMapper;\n    \n    @Test\n    @WithMockUser(roles = \"USER\")\n    void getUser_exists_returns200() throws Exception {\n        var user = User.builder().id(1L).email(\"test@test.com\").build();\n        var response = new UserResponse(1L, \"test@test.com\", \"Test\", UserStatus.ACTIVE, Instant.now());\n        \n        when(userService.findById(1L)).thenReturn(user);\n        when(userMapper.toResponse(user)).thenReturn(response);\n        \n        mockMvc.perform(get(\"/api/v1/users/1\"))\n            .andExpect(status().isOk())\n            .andExpect(jsonPath(\"$.email\").value(\"test@test.com\"));\n    }\n}\n```\n\n### Integration Test with Testcontainers\n\n```java\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@Testcontainers\nclass UserIntegrationTest {\n    \n    @Container\n    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(\"postgres:16-alpine\");\n    \n    @DynamicPropertySource\n    static void configureProperties(DynamicPropertyRegistry registry) {\n        registry.add(\"spring.datasource.url\", postgres::getJdbcUrl);\n        registry.add(\"spring.datasource.username\", postgres::getUsername);\n        registry.add(\"spring.datasource.password\", postgres::getPassword);\n    }\n    \n    @Autowired TestRestTemplate restTemplate;\n    \n    @Test\n    void fullUserLifecycle() {\n        // Create\n        var createReq = new CreateUserRequest(\"int@test.com\", \"Integration\", UserRole.USER);\n        var created = restTemplate.postForEntity(\"/api/v1/users\", createReq, UserResponse.class);\n        assertThat(created.getStatusCode()).isEqualTo(HttpStatus.CREATED);\n        \n        // Read\n        var fetched = restTemplate.getForEntity(\n            \"/api/v1/users/\" + created.getBody().id(), UserResponse.class);\n        assertThat(fetched.getBody().email()).isEqualTo(\"int@test.com\");\n    }\n}\n```\n\n### 7 Testing Rules\n\n1. **Constructor injection enables easy mocking** — no reflection hacks\n2. **Use `@WebMvcTest` for controller tests** — loads only web layer\n3. **Use `@DataJpaTest` for repository tests** — auto-configures JPA + rollback\n4. **Testcontainers for integration tests** — real Postgres/Redis, not H2\n5. **Test security** — `@WithMockUser`, `@WithAnonymousUser`\n6. **Test validation** — ensure `@Valid` rejects bad input\n7. **Don't test framework code** — test YOUR logic, not Spring's\n\n---\n\n## Phase 11: Performance Optimization\n\n### Priority Stack\n\n| # | Technique | Impact | Effort |\n|---|-----------|--------|--------|\n| 1 | Fix N+1 queries (JOIN FETCH / EntityGraph) | ★★★★★ | Low |\n| 2 | Add database indexes on filtered/sorted columns | ★★★★★ | Low |\n| 3 | Connection pool tuning (HikariCP) | ★★★★☆ | Low |\n| 4 | Redis caching for read-heavy data | ★★★★☆ | Medium |\n| 5 | DTO projections instead of full entities | ★★★★☆ | Medium |\n| 6 | Async processing for non-critical tasks (@Async) | ★★★☆☆ | Medium |\n| 7 | Virtual threads (Java 21+) for I/O-bound workloads | ★★★☆☆ | Low |\n| 8 | GraalVM native compilation for cold start | ★★★☆☆ | High |\n\n### Virtual Threads (Java 21+)\n\n```yaml\n# application.yml — enable virtual threads\nspring:\n  threads:\n    virtual:\n      enabled: true  # Tomcat uses virtual threads for requests\n```\n\n### Async Processing\n\n```java\n@Configuration\n@EnableAsync\npublic class AsyncConfig {\n    \n    @Bean\n    public TaskExecutor taskExecutor() {\n        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();\n        executor.setCorePoolSize(5);\n        executor.setMaxPoolSize(20);\n        executor.setQueueCapacity(100);\n        executor.setThreadNamePrefix(\"async-\");\n        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());\n        return executor;\n    }\n}\n\n@Async\npublic CompletableFuture<Report> generateReport(Long userId) {\n    // Runs on thread pool, doesn't block request thread\n    Report report = reportGenerator.generate(userId);\n    return CompletableFuture.completedFuture(report);\n}\n```\n\n---\n\n## Phase 12: Deployment\n\n### Multi-Stage Dockerfile\n\n```dockerfile\n# Build\nFROM eclipse-temurin:21-jdk-alpine AS build\nWORKDIR /app\nCOPY gradle/ gradle/\nCOPY gradlew build.gradle.kts settings.gradle.kts ./\nRUN ./gradlew dependencies --no-daemon  # Cache deps\nCOPY src/ src/\nRUN ./gradlew bootJar --no-daemon -x test\n\n# Runtime\nFROM eclipse-temurin:21-jre-alpine\nRUN addgroup -S app && adduser -S app -G app\nWORKDIR /app\nCOPY --from=build /app/build/libs/*.jar app.jar\nUSER app\nEXPOSE 8080\n\n# JVM tuning for containers\nENV JAVA_OPTS=\"-XX:+UseContainerSupport \\\n  -XX:MaxRAMPercentage=75.0 \\\n  -XX:InitialRAMPercentage=50.0 \\\n  -XX:+UseG1GC \\\n  -XX:+ExitOnOutOfMemoryError \\\n  -Djava.security.egd=file:/dev/./urandom\"\n\nENTRYPOINT [\"sh\", \"-c\", \"java $JAVA_OPTS -jar app.jar\"]\n```\n\n### GitHub Actions CI/CD\n\n```yaml\nname: CI/CD\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    services:\n      postgres:\n        image: postgres:16-alpine\n        env:\n          POSTGRES_DB: testdb\n          POSTGRES_USER: test\n          POSTGRES_PASSWORD: test\n        ports: ['5432:5432']\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n    \n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-java@v4\n        with:\n          distribution: temurin\n          java-version: 21\n          cache: gradle\n      \n      - name: Build & Test\n        run: ./gradlew build\n        env:\n          DATABASE_URL: jdbc:postgresql://localhost:5432/testdb\n          DATABASE_USERNAME: test\n          DATABASE_PASSWORD: test\n      \n      - name: Build Docker Image\n        if: github.ref == 'refs/heads/main'\n        run: |\n          docker build -t ${{ secrets.REGISTRY }}/app:${{ github.sha }} .\n          docker push ${{ secrets.REGISTRY }}/app:${{ github.sha }}\n```\n\n### Production Readiness Checklist\n\n**P0 — Mandatory:**\n- [ ] `open-in-view: false`\n- [ ] `ddl-auto: validate` + Flyway/Liquibase migrations\n- [ ] HikariCP pool configured with leak detection\n- [ ] Graceful shutdown enabled\n- [ ] Health + readiness endpoints exposed\n- [ ] Global exception handler (no stack traces in responses)\n- [ ] Input validation on all request DTOs\n- [ ] Security configured (auth, CORS, headers)\n- [ ] Structured JSON logging\n- [ ] Prometheus metrics exported\n\n**P1 — Within 30 days:**\n- [ ] Circuit breakers on external calls\n- [ ] Redis caching for hot paths\n- [ ] Virtual threads enabled (Java 21+)\n- [ ] Container resource limits set\n- [ ] Dependency vulnerability scanning in CI\n\n---\n\n## Phase 13: Kotlin-Specific Patterns\n\nIf using Kotlin instead of Java:\n\n```kotlin\n// Coroutines + WebFlux\n@RestController\n@RequestMapping(\"/api/v1/users\")\nclass UserController(private val userService: UserService) {\n    \n    @GetMapping(\"/{id}\")\n    suspend fun getUser(@PathVariable id: Long): UserResponse =\n        userService.findById(id).toResponse()\n    \n    @PostMapping\n    @ResponseStatus(HttpStatus.CREATED)\n    suspend fun createUser(@Valid @RequestBody request: CreateUserRequest): UserResponse =\n        userService.create(request).toResponse()\n}\n\n// Data classes as DTOs (no Lombok needed)\ndata class CreateUserRequest(\n    @field:NotBlank @field:Email\n    val email: String,\n    @field:NotBlank @field:Size(min = 2, max = 100)\n    val name: String,\n)\n\n// Extension functions for mapping\nfun User.toResponse() = UserResponse(\n    id = id,\n    email = email,\n    name = name,\n    status = status,\n    createdAt = createdAt,\n)\n```\n\n**Kotlin advantages**: null safety, data classes (no Lombok), coroutines for async, extension functions for mapping, sealed classes for error hierarchies.\n\n---\n\n## Phase 14: Advanced Patterns\n\n### Scheduled Jobs\n\n```java\n@Component\n@RequiredArgsConstructor\n@Slf4j\npublic class CleanupJob {\n    \n    private final UserRepository userRepository;\n    \n    @Scheduled(cron = \"0 0 2 * * *\")  // 2 AM daily\n    @SchedulerLock(name = \"cleanup\", lockAtMostFor = \"30m\")  // ShedLock for distributed\n    public void cleanupInactiveUsers() {\n        int count = userRepository.deactivateInactiveUsers(\n            UserStatus.INACTIVE,\n            Instant.now().minus(90, ChronoUnit.DAYS)\n        );\n        log.info(\"Deactivated {} inactive users\", count);\n    }\n}\n```\n\n### Kafka Integration\n\n```java\n@Component\n@RequiredArgsConstructor\npublic class OrderEventProducer {\n    \n    private final KafkaTemplate<String, OrderEvent> kafkaTemplate;\n    \n    public void publishOrderCreated(Order order) {\n        var event = new OrderEvent(\"ORDER_CREATED\", order.getId(), Instant.now());\n        kafkaTemplate.send(\"orders\", order.getId().toString(), event);\n    }\n}\n\n@Component\n@KafkaListener(topics = \"orders\", groupId = \"notification-service\")\npublic class OrderEventConsumer {\n    \n    @KafkaHandler\n    public void handleOrderEvent(OrderEvent event) {\n        // Process event with idempotency check\n    }\n}\n```\n\n### Multi-Tenancy\n\n```java\n@Component\npublic class TenantFilter extends OncePerRequestFilter {\n    \n    @Override\n    protected void doFilterInternal(HttpServletRequest request,\n                                     HttpServletResponse response,\n                                     FilterChain chain) throws ServletException, IOException {\n        String tenantId = request.getHeader(\"X-Tenant-ID\");\n        if (tenantId != null) {\n            TenantContext.setTenantId(tenantId);\n        }\n        try {\n            chain.doFilter(request, response);\n        } finally {\n            TenantContext.clear();\n        }\n    }\n}\n```\n\n---\n\n## 10 Common Mistakes\n\n| # | Mistake | Fix |\n|---|---------|-----|\n| 1 | `open-in-view: true` (default!) | Set `false` — prevents lazy loading outside transaction |\n| 2 | `ddl-auto: update` in production | Use Flyway/Liquibase — predictable, reversible migrations |\n| 3 | Field injection (`@Autowired`) | Constructor injection — testable, explicit dependencies |\n| 4 | Returning JPA entities from controllers | Use DTOs — prevents lazy loading errors + data leaks |\n| 5 | Not configuring HikariCP | Tune pool size, timeouts, leak detection |\n| 6 | Catching `Exception` everywhere | Specific exceptions + global handler |\n| 7 | No pagination on list endpoints | Always paginate — `Pageable` parameter |\n| 8 | Blocking calls in reactive stack | Don't mix blocking JPA with WebFlux |\n| 9 | Missing `@Transactional(readOnly=true)` | Optimizes read queries (no dirty checking) |\n| 10 | Testing with H2 instead of real DB | Testcontainers — H2 hides real SQL issues |\n\n---\n\n## Quality Rubric (0-100)\n\n| Dimension | Weight | Criteria |\n|-----------|--------|----------|\n| Architecture | 15% | Clean layers, DI, no circular deps |\n| Data Access | 15% | N+1 free, indexed, migrations managed |\n| Security | 15% | Auth, validation, headers, secrets management |\n| Testing | 15% | Pyramid coverage, Testcontainers, slice tests |\n| API Design | 10% | Consistent errors, pagination, OpenAPI docs |\n| Observability | 10% | Structured logs, metrics, health checks |\n| Resilience | 10% | Circuit breakers, retries, graceful shutdown |\n| Deployment | 10% | Containerized, CI/CD, zero-downtime |\n\n---\n\n## 10 Commandments of Spring Boot Production\n\n1. **Disable `open-in-view`** — first thing, every project\n2. **Constructor injection, always** — `@RequiredArgsConstructor`\n3. **DTOs at every boundary** — controllers never touch entities\n4. **`@Transactional(readOnly=true)` by default** — opt-in to writes\n5. **Testcontainers over H2** — test against real databases\n6. **Flyway for migrations** — never `ddl-auto: update`\n7. **Validate all input** — `@Valid` on every `@RequestBody`\n8. **Structure your logs** — JSON in production, MDC for context\n9. **Tune HikariCP** — pool size = (core_count * 2) + spindle_count\n10. **Enable graceful shutdown** — `server.shutdown: graceful`\n\n---\n\n## Natural Language Commands\n\nWhen working with Spring Boot projects, you can ask:\n\n1. `review my Spring Boot app` → Full architecture + config audit\n2. `check my JPA entities` → N+1, indexing, mapping review\n3. `review my security config` → Auth, CORS, headers, vulnerabilities\n4. `optimize my queries` → N+1 detection, projection opportunities\n5. `set up Testcontainers` → Integration test configuration\n6. `add caching` → Redis setup + cache strategy\n7. `add circuit breaker` → Resilience4j configuration\n8. `Dockerize my app` → Multi-stage Dockerfile + CI/CD\n9. `add observability` → Actuator + Prometheus + structured logging\n10. `review my tests` → Coverage gaps, missing slice tests\n11. `migrate to Java 21` → Virtual threads, pattern matching, records\n12. `convert to Kotlin` → Coroutines, data classes, extension functions\n\n---\n\n## ⚡ Level Up Your Spring Boot Skills\n\nThis free skill covers production engineering methodology. For **industry-specific AI agent context** that accelerates your Spring Boot projects:\n\n- **[SaaS Context Pack ($47)](https://afrexai-cto.github.io/context-packs/)** — SaaS billing, multi-tenancy, subscription management patterns\n- **[Fintech Context Pack ($47)](https://afrexai-cto.github.io/context-packs/)** — Payment processing, compliance, financial data patterns\n- **[Healthcare Context Pack ($47)](https://afrexai-cto.github.io/context-packs/)** — HIPAA compliance, HL7/FHIR, audit logging patterns\n\n## 🔗 More Free Skills by AfrexAI\n\n- `afrexai-python-production` — Python production engineering\n- `afrexai-api-architecture` — API design & architecture\n- `afrexai-database-engineering` — Database optimization & scaling\n- `afrexai-test-automation-engineering` — Test strategy & automation\n- `afrexai-cicd-engineering` — CI/CD pipeline engineering\n\nBrowse all: [AfrexAI on ClawHub](https://clawhub.com) | [Context Packs Storefront](https://afrexai-cto.github.io/context-packs/)\n","tags":{"enterprise":"1.0.0","java":"1.0.0","jpa":"1.0.0","kotlin":"1.0.0","latest":"1.0.0","production":"1.0.0","spring-boot":"1.0.0"},"stats":{"comments":0,"downloads":202,"installsAllTime":8,"installsCurrent":0,"stars":0,"versions":1},"createdAt":1771695022905,"updatedAt":1778491601423},"latestVersion":{"version":"1.0.0","createdAt":1771695022905,"changelog":"Spring Boot Production Engineering methodology now available as a skill:\n\n- Added a comprehensive methodology for production-ready Spring Boot (Java/Kotlin) apps: covers architecture, security, observability, testing, deployment, and performance.\n- Includes a health check scoring table to quickly assess your app's readiness.\n- Provides project structure recommendations, key architecture rules, and guidance for dependency selection.\n- Offers detailed production configuration templates and profile strategies for different environments.\n- Contains best practices for JPA entity design and database configuration.\n- Ideal for teams wanting a clear, modern blueprint for robust, secure, and maintainable Spring Boot services.","license":null},"metadata":null,"owner":{"handle":"1kalin","userId":"s17e1q0nx23qnh4n429zzqc05x83hvsw","displayName":"1kalin","image":"https://avatars.githubusercontent.com/u/15705344?v=4"},"moderation":null}