AI Context: This is the single source of truth for architectural patterns. For current status: see
../docs/status/progress.yamlFor system overview: see../docs/architecture/overview.md
This Java Spring Boot boilerplate follows established enterprise patterns to ensure maintainability, scalability, and consistency across the codebase.
┌─────────────────────────────────────┐
│ Controller Layer (REST) │ ← HTTP Requests/Responses
├─────────────────────────────────────┤
│ Service Layer │ ← Business Logic
├─────────────────────────────────────┤
│ Repository Layer │ ← Data Access
├─────────────────────────────────────┤
│ Model Layer │ ← Domain Entities
└─────────────────────────────────────┘
Controller Layer
- Handle HTTP requests and responses
- Validate request data
- Map requests to service calls
- Return appropriate HTTP status codes
- API documentation with OpenAPI
Service Layer
- Implement business logic
- Coordinate multiple repositories
- Handle transactions
- Apply business rules and validation
- Manage caching
Repository Layer
- Data access and persistence
- Database queries
- Entity mapping
- Transaction management
Model Layer
- Domain entities (JPA entities)
- DTOs for data transfer
- Value objects
- Enums and constants
// Separate DTOs for different operations
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@NotBlank String password
) {}
public record UpdateUserRequest(
@Size(min = 2, max = 100) String name,
@Email String email
) {}
public record UserResponse(
Long id,
String name,
String email,
UserRole role,
LocalDateTime createdAt
) {}
// Use MapStruct or manual mappers
@Mapper(componentModel = "spring")
public interface UserMapper {
UserResponse toResponse(User user);
User toEntity(CreateUserRequest request);
}@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Query methods
Optional<User> findByEmail(String email);
List<User> findByRole(UserRole role);
@Query("SELECT u FROM User u WHERE u.active = true AND u.role = :role")
List<User> findActiveUsersByRole(@Param("role") UserRole role);
// Custom query with pagination
Page<User> findByNameContainingIgnoreCase(String name, Pageable pageable);
// Exists query
boolean existsByEmail(String email);
}
// Custom repository for complex queries
@Repository
public interface UserRepositoryCustom {
List<User> findUsersByComplexCriteria(UserSearchCriteria criteria);
}
@RequiredArgsConstructor
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
private final EntityManager entityManager;
@Override
public List<User> findUsersByComplexCriteria(UserSearchCriteria criteria) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);
List<Predicate> predicates = new ArrayList<>();
if (criteria.name() != null) {
predicates.add(cb.like(
cb.lower(user.get("name")),
"%" + criteria.name().toLowerCase() + "%"
));
}
if (criteria.role() != null) {
predicates.add(cb.equal(user.get("role"), criteria.role()));
}
query.where(predicates.toArray(new Predicate[0]));
return entityManager.createQuery(query).getResultList();
}
}// Service interface
public interface UserService {
UserResponse findById(Long id);
UserResponse createUser(CreateUserRequest request);
UserResponse updateUser(Long id, UpdateUserRequest request);
void deleteUser(Long id);
Page<UserResponse> findAll(Pageable pageable);
}
// Service implementation
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
private final EmailService emailService;
@Override
@Transactional(readOnly = true)
public UserResponse findById(Long id) {
log.debug("Finding user by id: {}", id);
return userRepository.findById(id)
.map(userMapper::toResponse)
.orElseThrow(() -> new UserNotFoundException(id));
}
@Override
@Transactional
public UserResponse createUser(CreateUserRequest request) {
log.info("Creating user with email: {}", request.email());
validateEmailUniqueness(request.email());
User user = userMapper.toEntity(request);
User savedUser = userRepository.save(user);
// Async operation
emailService.sendWelcomeEmail(savedUser);
return userMapper.toResponse(savedUser);
}
private void validateEmailUniqueness(String email) {
if (userRepository.existsByEmail(email)) {
throw new EmailAlreadyExistsException(email);
}
}
}// Strategy interface
public interface PaymentStrategy {
PaymentResult process(PaymentRequest request);
boolean supports(PaymentMethod method);
}
// Concrete strategies
@Service
public class CreditCardPaymentStrategy implements PaymentStrategy {
@Override
public PaymentResult process(PaymentRequest request) {
// Process credit card payment
return new PaymentResult(true, "Payment processed");
}
@Override
public boolean supports(PaymentMethod method) {
return method == PaymentMethod.CREDIT_CARD;
}
}
@Service
public class PayPalPaymentStrategy implements PaymentStrategy {
@Override
public PaymentResult process(PaymentRequest request) {
// Process PayPal payment
return new PaymentResult(true, "Payment processed via PayPal");
}
@Override
public boolean supports(PaymentMethod method) {
return method == PaymentMethod.PAYPAL;
}
}
// Strategy context
@Service
@RequiredArgsConstructor
public class PaymentService {
private final List<PaymentStrategy> strategies;
public PaymentResult processPayment(PaymentRequest request) {
return strategies.stream()
.filter(strategy -> strategy.supports(request.method()))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentMethodException(request.method()))
.process(request);
}
}// Using Lombok @Builder
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
private BigDecimal totalAmount;
private OrderStatus status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items;
@CreationTimestamp
private LocalDateTime createdAt;
}
// Usage
Order order = Order.builder()
.orderNumber("ORD-001")
.totalAmount(new BigDecimal("99.99"))
.status(OrderStatus.PENDING)
.items(List.of(item1, item2))
.build();@Component
public class UserFactory {
public User createUser(CreateUserRequest request, UserRole role) {
return User.builder()
.name(request.name())
.email(request.email())
.password(encodePassword(request.password()))
.role(role)
.active(true)
.build();
}
public User createAdmin(CreateUserRequest request) {
return createUser(request, UserRole.ADMIN);
}
public User createClient(CreateUserRequest request) {
return createUser(request, UserRole.CLIENT);
}
private String encodePassword(String rawPassword) {
// Password encoding logic
return rawPassword; // Replace with actual encoding
}
}// Event
@Getter
public class UserCreatedEvent extends ApplicationEvent {
private final User user;
public UserCreatedEvent(Object source, User user) {
super(source);
this.user = user;
}
}
// Publisher
@Service
@RequiredArgsConstructor
public class UserService {
private final ApplicationEventPublisher eventPublisher;
private final UserRepository userRepository;
@Transactional
public User createUser(CreateUserRequest request) {
User user = userRepository.save(new User(request));
// Publish event
eventPublisher.publishEvent(new UserCreatedEvent(this, user));
return user;
}
}
// Listeners
@Component
@Slf4j
public class UserCreatedEventListener {
@Async
@EventListener
public void handleUserCreated(UserCreatedEvent event) {
log.info("New user created: {}", event.getUser().getEmail());
// Send welcome email
}
}
@Component
@Slf4j
public class UserAnalyticsListener {
@Async
@EventListener
public void handleUserCreated(UserCreatedEvent event) {
log.info("Recording analytics for user: {}", event.getUser().getId());
// Record analytics
}
}public class UserSpecifications {
public static Specification<User> hasName(String name) {
return (root, query, cb) ->
name == null ? null : cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<User> hasRole(UserRole role) {
return (root, query, cb) ->
role == null ? null : cb.equal(root.get("role"), role);
}
public static Specification<User> isActive(Boolean active) {
return (root, query, cb) ->
active == null ? null : cb.equal(root.get("active"), active);
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
}
// Repository with Specification support
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
// Usage in service
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public List<User> searchUsers(UserSearchCriteria criteria) {
Specification<User> spec = Specification.where(null);
spec = spec.and(UserSpecifications.hasName(criteria.name()));
spec = spec.and(UserSpecifications.hasRole(criteria.role()));
spec = spec.and(UserSpecifications.isActive(criteria.active()));
spec = spec.and(UserSpecifications.createdAfter(criteria.createdAfter()));
return userRepository.findAll(spec);
}
}@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "products");
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@CachePut(value = "users", key = "#result.id")
public User updateUser(Long id, UpdateUserRequest request) {
User user = findById(id);
// Update user
return userRepository.save(user);
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@CacheEvict(value = "users", allEntries = true)
public void clearCache() {
// Clear all user cache
}
}@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@Service
@Slf4j
public class EmailService {
@Async("taskExecutor")
public CompletableFuture<Void> sendWelcomeEmail(User user) {
log.info("Sending welcome email to: {}", user.getEmail());
try {
// Send email logic
Thread.sleep(2000); // Simulate delay
log.info("Email sent successfully to: {}", user.getEmail());
} catch (Exception e) {
log.error("Error sending email", e);
}
return CompletableFuture.completedFuture(null);
}
}@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreaker externalServiceCircuitBreaker() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.slidingWindowSize(2)
.build();
return CircuitBreaker.of("externalService", config);
}
}
@Service
@RequiredArgsConstructor
public class ExternalApiService {
private final CircuitBreaker circuitBreaker;
private final RestTemplate restTemplate;
public ApiResponse callExternalApi(String endpoint) {
return circuitBreaker.executeSupplier(() -> {
try {
return restTemplate.getForObject(endpoint, ApiResponse.class);
} catch (Exception e) {
throw new ExternalApiException("Failed to call external API", e);
}
});
}
}@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserResponse>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id,asc") String[] sort
) {
Pageable pageable = PageRequest.of(
page,
size,
Sort.by(parseSortOrders(sort))
);
Page<UserResponse> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
private Sort.Order[] parseSortOrders(String[] sort) {
return Arrays.stream(sort)
.map(s -> {
String[] parts = s.split(",");
String property = parts[0];
Sort.Direction direction = parts.length > 1 && "desc".equalsIgnoreCase(parts[1])
? Sort.Direction.DESC
: Sort.Direction.ASC;
return new Sort.Order(direction, property);
})
.toArray(Sort.Order[]::new);
}
}- Layered Architecture: Always use for enterprise applications
- DTO Pattern: Use for all API request/response objects
- Repository Pattern: Use Spring Data JPA for data access
- Service Pattern: Implement business logic in service layer
- Strategy Pattern: Use for interchangeable algorithms
- Builder Pattern: Use for complex object construction
- Factory Pattern: Use for object creation logic
- Observer Pattern: Use for event-driven architecture
- Specification Pattern: Use for dynamic query building
- Caching Pattern: Use for frequently accessed data
- Async Pattern: Use for time-consuming operations
- Circuit Breaker: Use for external service calls
- ❌ Putting business logic in controllers
- ❌ Exposing entities directly in REST APIs
- ❌ Using field injection instead of constructor injection
- ❌ Not using transactions for multi-operation methods
- ❌ Ignoring exception handling
- ❌ Not implementing pagination for list endpoints
- ❌ Tight coupling between layers
- ❌ Not using DTOs for data transfer
Last Updated: 2025-10-08