Skip to Content
BackendGolden PathsJava Spring Boot

Java Spring Boot

Stack: Spring Boot 3 · Java 21 · Spring Data JPA · Spring Security · Maven

Overview

The Java Spring Boot golden path is the standard for building enterprise-grade REST and event-driven microservices at Stratpoint. It provides a proven, opinionated structure covering layered architecture, design patterns, concurrency, security, and cloud-native resilience — all following Spring Boot 3 and Java 21 best practices.

When to Use

✅ Use this when❌ Avoid this when
Building enterprise REST or event-driven microservicesLightweight scripts or small serverless functions
Java is the team’s primary languageTeam is primarily TypeScript or Python
You need Spring Security, JPA, and AOP out of the boxLow-latency reactive pipelines (prefer Spring WebFlux or Quarkus)
Cloud-native patterns are required (circuit breaker, saga, modular monolith)Rapid prototype with minimal setup overhead

Tech Stack

LayerTechnologyVersion
FrameworkSpring Boot3.x
LanguageJava21
ORMSpring Data JPA / Hibernate
Build ToolMaven3.8+
DatabasePostgreSQL14+
Schema MigrationFlyway or Liquibase
SecuritySpring Security
ResilienceResilience4j
ObservabilitySpring Actuator · Micrometer · Zipkin
TestingJUnit 5 · Mockito · TestContainers
ContainerizationDocker
OrchestrationKubernetes (Kustomize)

Prerequisites

  • Java 21 installed (via SDKMAN  or Homebrew)
  • Maven 3.8+ installed
  • Docker Desktop installed and running
  • IntelliJ IDEA with Lombok plugin enabled
  • PostgreSQL client (e.g., DBeaver or psql)

Scaffolding

The boilerplate for this golden path lives at stratpoint-engineering/golden-paths .

npx degit stratpoint-engineering/golden-paths/backend/java-springboot/reference my-app cd my-app cp .env.example .env docker compose up -d

The Stratpoint MCP tool can also scaffold this automatically — the manifest.json in the repo root drives that flow.

Project Structure

    • PULL_REQUEST_TEMPLATE.md
  • .editorconfig
  • .gitignore
  • Dockerfile
  • docker-compose.yml
  • pom.xml
  • README.md

Local Development

Environment Variables

# application-dev.yml spring: datasource: url: jdbc:postgresql://localhost:5432/appdb username: postgres password: postgres jpa: hibernate: ddl-auto: validate show-sql: false profiles: active: dev logging: level: root: INFO com.company.app: DEBUG pattern: console: '{"timestamp":"%d","level":"%p","traceId":"%X{traceId}","message":"%m"}%n'

Running Locally

# docker-compose.yml version: '3.8' services: app: build: . ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=dev - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/appdb - SPRING_DATASOURCE_USERNAME=postgres - SPRING_DATASOURCE_PASSWORD=postgres depends_on: - db volumes: - ./logs:/app/logs db: image: postgres:14-alpine ports: - "5432:5432" environment: - POSTGRES_DB=appdb - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data:
docker compose up

Conventions

Naming

TargetConventionExample
ClassesPascalCaseUserService
Methods / VariablescamelCasegetUserById
ConstantsUPPER_SNAKE_CASEMAX_RETRY_COUNT
Packageslowercase with dotscom.company.module
RepositoriesSuffix RepositoryUserRepository
ServicesSuffix ServiceUserService
ControllersSuffix ControllerUserController

Patterns

The codebase is organized into four strict layers. Each layer communicates only with the layer directly below it and uses the correct data type at each boundary.

  1. Controller Layer — Handles HTTP requests/responses, input validation, and delegates to services. Uses DTOs. Contains no business logic. Uses @ControllerAdvice for centralized error handling.
  2. Service Layer — Implements business logic and transaction management. Receives DTOs from controllers, works with Entities internally, returns DTOs to controllers.
  3. Repository Layer — Data access via Spring Data JPA. Custom JPQL queries where needed. Returns Entities to the service layer.
  4. Model Layer — JPA Entities for database mapping, DTOs for API request/response, Records for cross-service payloads, Mappers to convert between Entities and DTOs.

Code Style

  • Use Lombok (@RequiredArgsConstructor, @Getter, @Builder) to reduce boilerplate
  • Use constructor injection exclusively — never @Autowired on fields
  • Inject services via their interface type, not the concrete implementation
  • Use @ControllerAdvice + @ExceptionHandler for all error responses
  • Use structured JSON logging with SLF4J + Logback

Architectural Patterns

Dependency Injection (DI)

Goal: Ensure loose coupling and testability through constructor-based dependency management.

Best Practice: Use constructor injection to guarantee immutability, thread-safety, and eliminate null-states. Use @RequiredArgsConstructor from Lombok to avoid manual constructor boilerplate.

@Service("myService") public class MyServiceImpl implements MyService { private final MyComponent myAction; private final MyComponent myApproach; public MyServiceImpl( @Qualifier("myAction") MyComponent myAction, @Qualifier("myApproach") MyComponent myApproach ) { this.myAction = myAction; this.myApproach = myApproach; } public String testMe() { return myAction.doIt() + " " + myApproach.doIt(); } } @Component("myAction") public class MyActionImpl implements MyComponent { public String doIt() { return "I'm ready for best practices!"; } } @Component("myApproach") public class MyApproachImpl implements MyComponent { public String doIt() { return "Constructor Injection!"; } }

Proxy Pattern (AOP)

Goal: Abstract cross-cutting concerns (transactions, async, security checks) without polluting business logic.

Best Practice: Use Spring AOP to implement cross-cutting concerns like @Transactional, logging, and security checks via custom annotations. This reduces redundant validation in business logic implementations.

// Custom Annotation @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckUserSession { String requiredRole() default "USER"; } // Aspect @Aspect @Component public class SessionProxyAspect { private final HttpServletRequest request; private final RedisService redisService; public SessionProxyAspect( @Autowired HttpServletRequest request, @Qualifier("redisService") RedisService redisService ) { this.request = request; this.redisService = redisService; } @Around("@annotation(checkSession)") public Object validateSession(ProceedingJoinPoint joinPoint, CheckUserSession checkSession) throws Throwable { String sessionId = request.getHeader("X-Session-ID"); String currentRole = redisService.getSessionRole(sessionId); if (currentRole == null || !currentRole.equals(checkSession.requiredRole())) { throw new UnauthorizedAccessException("Required role: " + checkSession.requiredRole()); } return joinPoint.proceed(); } } @Service public class SensitiveDataService { @CheckUserSession(requiredRole = "ADMIN") public Data getFinancialReports() { // Business logic only — no session checks needed here return repository.fetchSecretData(); } }

Self-invocation bypass: @Transactional and @Async only work when called from a different bean. Calling an annotated method from within the same class bypasses the proxy entirely.


Factory Pattern

Goal: Centralize complex object creation while hiding implementation details from the caller.

Best Practice: Leverage Spring-managed beans as factory implementations. Use a Map<String, ServiceInterface> injected via the constructor to replace if/else or switch blocks. Since Spring beans are singleton-scoped by default, this ensures low memory utilization.

public interface NotificationProvider { void send(String recipient, String message); } @Service("SMS") public class SmsNotificationService implements NotificationProvider { @Override public void send(String r, String m) { /* ... */ } } @Service("EMAIL") public class EmailNotificationService implements NotificationProvider { @Override public void send(String r, String m) { /* ... */ } } @Service("PUSH") public class PushNotificationService implements NotificationProvider { @Override public void send(String r, String m) { /* ... */ } } @Service("notificationServiceFactory") public class NotificationFactory { // Spring DI automatically maps all classes implementing NotificationProvider private final Map<String, NotificationProvider> providers; public NotificationFactory(Map<String, NotificationProvider> providers) { this.providers = providers; } public void notify(String type, String recipient, String message) { NotificationProvider provider = providers.get(type.toUpperCase()); if (provider == null) { throw new UnsupportedOperationException("No provider found for: " + type); } provider.send(recipient, message); } }

Observer Pattern

Goal: Maintain loose coupling between primary business logic and secondary side-effects.

Best Practice:

  • Use Java Records for all event payloads (thread-safe, immutable)
  • Configure a Virtual Thread Task Executor for @Async scalability
  • Enable @EnableAsync and @EnableTransactionManagement explicitly
@Configuration @EnableAsync @EnableTransactionManagement public class AsyncEventConfig { @Bean public Executor taskExecutor() { return Executors.newVirtualThreadPerTaskExecutor(); } } // Event payload as a Record (immutable, thread-safe) public record UserRegistrationEvent( Long userId, String email, String status, LocalDateTime timestamp ) {}

Observers:

  • @TransactionalEventListener(phase = AFTER_COMMIT) for external side-effects (email, SMS) — fires only after DB commit
  • @EventListener for internal auditing and metrics — fires immediately
  • @Async on all listeners to maximize throughput
@Component @Slf4j public class UserObserver { @Async @EventListener(condition = "#event.status == 'PENDING'") public void logAuditAttempt(UserRegistrationEvent event) { log.info("[AUDIT] Registration attempt for User ID: {}", event.userId()); } @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void sendWelcomeEmail(UserRegistrationEvent event) { log.info("[EXTERNAL] Sending Welcome Email to {}...", event.email()); // Call Email Service / SMTP } }

Publishers:

  • Use @Transactional for state-changing operations (e.g., DB insertions)
  • Never perform slow I/O inside a @Transactional method
  • Never use @Transactional for read-only operations
@Service @RequiredArgsConstructor public class RegistrationService { private final ApplicationEventPublisher eventPublisher; private final UserRepository userRepository; @Transactional public void registerNewUser(String email) { User user = userRepository.save(new User(email, "PENDING")); eventPublisher.publishEvent(new UserRegistrationEvent( user.getId(), email, user.getStatus(), LocalDateTime.now() )); } }

Builder Pattern

Goal: Facilitate creation of valid, immutable-first DTOs with strict guardrails.

Best Practice:

  • Use @AllArgsConstructor(access = AccessLevel.PRIVATE) to force the builder
  • Use @NoArgsConstructor(access = AccessLevel.PROTECTED) for JPA/Jackson reflection
  • Override .build() to run validation before returning the object
  • Use @Setter(AccessLevel.PROTECTED) for attributes that need post-creation mutation
@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder(toBuilder = true) @JsonDeserialize(builder = UserAccountDTO.UserAccountDTOBuilder.class) @ValidUserAccount public class UserAccountDTO { @NotBlank(message = "Email is required") @Email(message = "Invalid email format") private String email; @NotBlank(message = "Mobile number is required") @Pattern( regexp = "^(\\+63-9|09)\\d{9}$", message = "Mobile must follow +63-9XXXXXXXXX or 09XXXXXXXXX format" ) private String mobileNumber; @Setter(AccessLevel.PROTECTED) @Builder.Default private String status = "INACTIVE"; // Self-validation on build public static class UserAccountDTOBuilder { public UserAccountDTO build() { UserAccountDTO dto = new UserAccountDTO(email, mobileNumber, status); ValidationUtils.validate(dto); return dto; } } }

Shared validator utility:

public class ValidationUtils { private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); public static <T> void validate(T object) { Set<ConstraintViolation<T>> violations = VALIDATOR.validate(object); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } } }

Concurrency

Virtual Thread Concurrency (Java 21)

Goal: Improve throughput for I/O-bound workloads (DB calls, external API calls, file reads) without the overhead of platform thread pools.

Best Practice: Use Executors.newVirtualThreadPerTaskExecutor() or configure Spring’s task executor via application.yml.

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); executor.submit(() -> System.out.println("Task running in virtual thread")); executor.shutdown();
# application.yml spring: task: execution: type: virtual pool: keep-alive: 60s max-size: 1000

Structured Concurrency (Java 21)

Goal: Treat a group of related concurrent tasks as a single unit of work. If one subtask fails, all others are automatically cancelled — no orphan threads.

Best Practice:

  • Use ShutdownOnSuccess when only one result is needed
  • Use ShutdownOnFailure when all results are required
public Response getProfile(String id) throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Subtask<User> user = scope.fork(() -> fetchUser(id)); Subtask<Order> order = scope.fork(() -> fetchLatestOrder(id)); scope.join(); scope.throwIfFailed(); return new Response(user.get(), order.get()); } }

Circuit Breaker Pattern (Resilience4j)

Goal: Prevent cascading failures across microservices by detecting and isolating failing dependencies.

Best Practice:

  • Use COUNT_BASED for low-traffic services, TIME_BASED for high-traffic
  • Only trip the circuit on infrastructure errors — never on business validation errors
# application.yml resilience4j: circuitbreaker: instances: externalOrderService: registerHealthIndicator: true slidingWindowSize: 10 failureRateThreshold: 50 waitDurationInOpenState: 10000ms permittedNumberOfCallsInHalfOpenState: 3 slidingWindowType: COUNT_BASED
@Service @RequiredArgsConstructor public class OrderService { private final ExternalApiClient apiClient; @CircuitBreaker(name = "externalOrderService", fallbackMethod = "handleApiFailure") public OrderResponse processOrder(OrderRequest request) { return apiClient.submit(request); } private OrderResponse handleApiFailure(OrderRequest request, Throwable t) { log.error("Circuit Breaker triggered. Reason: {}", t.getMessage()); return OrderResponse.builder() .status("QUEUED_LOCAL") .message("Provider is down; order cached for retry.") .build(); } }

Enterprise / Cloud-Native Patterns

PatternToolPurpose
API GatewaySpring Cloud GatewayManage, route, and secure requests in a microservices environment
SagaCustom / ChoreographyManage distributed transactions across services for eventual consistency
Modular MonolithSpring ModulithSingle deployable unit with strictly enforced module boundaries

Database

JPA / Hibernate Configuration

  • Use LAZY fetch type by default — only fetch eagerly when strictly necessary
  • Configure HikariCP connection pooling with appropriate pool sizes
  • Set query timeout values to prevent long-running queries

Schema Management

  • Use Flyway or Liquibase for all schema migrations
  • Version every schema change with a sequential migration file
  • Always include a rollback script for destructive changes

Performance Optimization

  • Add indexes for columns used in WHERE, JOIN, and ORDER BY clauses
  • Implement pagination for all list endpoints (Pageable)
  • Use query projections (interfaces or DTOs) instead of full entity fetches where only a subset of fields is needed
  • Use query caching where appropriate

Testing

Coverage Targets

LayerMinimum Coverage
Overall80%
Service Layer90%
Critical Business Logic100%

Test Types

TypeToolNotes
UnitJUnit 5 + MockitoTest positive and negative scenarios; use parameterized tests for boundaries
Integration@SpringBootTest + TestContainersReal database; clean up test data after each test
API / ControllerMockMvc or RestAssuredTest all endpoints, verify response structure and status codes
E2ECustom integration suiteCritical user flows only

CI/CD

# .github/workflows/ci.yml name: Java Spring Boot CI/CD on: push: branches: [main, develop] pull_request: branches: [main, develop] jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 21 uses: actions/setup-java@v3 with: java-version: '21' distribution: 'temurin' cache: 'maven' - name: Code quality checks run: mvn checkstyle:check pmd:check spotbugs:check - name: Dependency vulnerability check run: mvn dependency-check:check - name: Unit tests run: mvn test - name: Test coverage report run: mvn jacoco:report - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 build: needs: quality runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 21 uses: actions/setup-java@v3 with: java-version: '21' distribution: 'temurin' cache: 'maven' - name: Build and integration tests run: mvn verify - name: Build Docker image run: docker build -t myapp:${{ github.sha }} . - name: Container security scan uses: aquasecurity/trivy-action@master with: image-ref: 'myapp:${{ github.sha }}' format: 'table' exit-code: '1' severity: 'CRITICAL,HIGH' deploy: needs: build if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') runs-on: ubuntu-latest environment: name: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} steps: - uses: actions/checkout@v3 - name: Set up kubectl uses: azure/setup-kubectl@v3 - name: Set Kubernetes context uses: azure/k8s-set-context@v2 with: kubeconfig: ${{ secrets.KUBE_CONFIG }} - name: Deploy to environment run: | cd k8s/overlays/${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }} kustomize build | kubectl apply -f - - name: Verify deployment run: | kubectl rollout status deployment/myapp -n ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}

Observability

Metrics

Use Spring Boot Actuator with Micrometer to expose metrics for Prometheus:

management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always

Track key business metrics, API response times, and database operation times via Micrometer counters and timers.

Distributed Tracing

Use Spring Cloud Sleuth with Zipkin to trace requests across services. Include correlation IDs in all log entries.

Logging Strategy

EnvironmentLog LevelFormat
devDEBUGHuman-readable
testINFOStructured JSON
stagingINFOStructured JSON
prodWARN / ERRORStructured JSON
  • Use SLF4J + Logback with JSON output in non-dev environments
  • Include traceId, spanId, and correlation IDs in every log entry
  • Log start/end of significant operations and all exceptions with stack traces (non-prod only)
  • Never log sensitive data (PII, credentials, tokens)

Security

Authentication

  • Use JWT-based authentication with Spring Security
  • Implement token-based session management with refresh token rotation
  • Encode passwords with BCrypt
  • Add account lockout after repeated failed attempts

Authorization

  • Define clear roles (USER, ADMIN) and use @PreAuthorize for method-level security
  • Implement fine-grained permissions via Spring Security’s expression-based access control
  • Use HTTPS for all endpoints
  • Configure CORS explicitly — never use wildcard * in production
  • Add rate limiting and security headers (CSRF, XSS protection)

Data Protection

  • Encrypt sensitive data at rest using attribute-level encryption where needed
  • Implement proper key management (AWS KMS, HashiCorp Vault)
  • Validate all input using Bean Validation (JSR-380) and custom validators
  • Never log sensitive fields (passwords, tokens, card numbers)

Containerization

# Multi-stage build for optimized image size FROM maven:3.8.5-openjdk-21-slim AS build WORKDIR /app COPY pom.xml . # Download dependencies separately for better layer caching RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # Runtime image FROM eclipse-temurin:21-jre-alpine WORKDIR /app # Add non-root user RUN addgroup -S spring && adduser -S spring -G spring USER spring:spring # Copy built artifact from build stage COPY --from=build /app/target/*.jar app.jar # Configure health check HEALTHCHECK --interval=30s --timeout=3s \ CMD wget -q --spider http://localhost:8080/actuator/health || exit 1 # Run with container-aware JVM options ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]

AI-Assisted Development

Claude Code

Recommended uses:

  • Scaffolding controllers, services, repositories, and DTOs following the layer structure above
  • Generating boilerplate (Lombok DTOs, JUnit 5 tests, Flyway migration scripts, CI/CD pipelines)
  • Explaining AOP proxy behavior, structured concurrency, and circuit breaker configuration
  • Drafting unit and integration tests with JUnit 5, Mockito, and TestContainers
  • Code review — ask Claude to check against the Implementation Checklist in the Code Review Checklist

CLAUDE.md setup:

# CLAUDE.md ## Stack Java 21 · Spring Boot 3 · Spring Data JPA · Maven ## Project Structure Layered architecture: controller → service (interface + impl) → repository → model (entity/dto/mapper) Packages: config, controller/v1, model/entity, model/dto, model/mapper, repository, service, service/impl, exception, security, util ## Key Conventions - PascalCase for classes, camelCase for methods/variables, UPPER_SNAKE_CASE for constants - Suffix classes: UserRepository, UserService, UserController - Constructor injection only — never @Autowired on fields - Inject services via their interface type, not the concrete impl ## What to follow - Use @RequiredArgsConstructor for constructor injection - Use Java Records for event payloads (Observer pattern) - Use @TransactionalEventListener(phase = AFTER_COMMIT) for external side-effects - Use Executors.newVirtualThreadPerTaskExecutor() for @Async task executors - Use @Builder with @AllArgsConstructor(access = PRIVATE) for DTOs - Validate DTOs via ValidationUtils.validate() in the builder's build() method ## What to avoid - Field injection with @Autowired - @Transactional on read-only operations - Slow I/O inside @Transactional methods - Public setters on DTOs — use .toBuilder() instead - Static factory methods that bypass the Spring Application Context - Sharing a circuit breaker instance across different external APIs

Guardrails:

  • Do not let Claude generate Spring Security configuration without manual review
  • Always review AI-generated JPQL / native SQL queries before merging
  • Do not share production secrets, PII, or proprietary data in prompts

Cursor

Recommended uses:

  • Inline generation of controller endpoints, service methods, and repository queries
  • Refactoring field injection to constructor injection across the codebase
  • Asking @codebase questions about service dependencies and data flow
  • Generating parameterized JUnit 5 tests for boundary conditions

.cursor/rules setup:

## Role You are a Java Spring Boot engineer on an enterprise Spring Boot 3 / Java 21 project at Stratpoint. ## Standards to follow - Always use constructor injection. Never @Autowired on fields. - Use @RequiredArgsConstructor (Lombok) to reduce boilerplate. - Services must implement an interface; inject via the interface type. - Use Java Records for event payloads. Use @Builder with private constructors for DTOs. - Use @TransactionalEventListener(phase = AFTER_COMMIT) for external side-effects. - Use Executors.newVirtualThreadPerTaskExecutor() for all @Async task executors. - Never place slow I/O inside a @Transactional method. ## Code style - PascalCase for classes, camelCase for methods/variables, UPPER_SNAKE_CASE for constants - Suffix: UserRepository, UserService, UserController, UserMapper ## Always - Write JUnit 5 tests alongside any new service or controller - Validate DTOs using Bean Validation + custom validators - Use structured JSON logging with SLF4J + Logback and include correlation IDs ## Never - Use field injection (@Autowired on private fields) - Use @Transactional on read-only service methods - Hardcode secrets — use Spring profiles and environment variables - Skip input validation at controller or service entry points

Guardrails:

  • Review all AI-suggested pom.xml dependency additions before accepting
  • Do not use AI-generated Spring Security configuration without a dedicated review pass
  • Treat suggestions as a first draft — validate against the Code Review Checklist

References

Official Documentation

Internal Resources

Further Reading

Last updated on