Skip to content

Latest commit

 

History

History
647 lines (510 loc) · 16.8 KB

File metadata and controls

647 lines (510 loc) · 16.8 KB

Architectural Patterns - Java Spring Boot Boilerplate

AI Context: This is the single source of truth for architectural patterns. For current status: see ../docs/status/progress.yaml For system overview: see ../docs/architecture/overview.md

Pattern Overview

This Java Spring Boot boilerplate follows established enterprise patterns to ensure maintainability, scalability, and consistency across the codebase.

Layered Architecture

Architecture Layers

┌─────────────────────────────────────┐
│      Controller Layer (REST)        │  ← HTTP Requests/Responses
├─────────────────────────────────────┤
│         Service Layer               │  ← Business Logic
├─────────────────────────────────────┤
│       Repository Layer              │  ← Data Access
├─────────────────────────────────────┤
│         Model Layer                 │  ← Domain Entities
└─────────────────────────────────────┘

Layer Responsibilities

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

Data Transfer Object (DTO) Pattern

Request/Response DTOs

// 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 Pattern

Spring Data JPA Repositories

@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 Pattern

Service Interface and Implementation

// 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 Pattern

Payment Processing Example

// 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);
    }
}

Builder Pattern

Complex Object Creation

// 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();

Factory Pattern

Entity Factory

@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
    }
}

Observer Pattern (Event-Driven)

Spring Events

// 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
    }
}

Specification Pattern

Dynamic Query Building

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);
    }
}

Caching Pattern

Spring Cache Abstraction

@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
    }
}

Async Pattern

Asynchronous Processing

@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);
    }
}

Circuit Breaker Pattern

Resilience4j Integration

@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);
            }
        });
    }
}

Pagination Pattern

Spring Data Pagination

@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);
    }
}

Best Practices

Pattern Selection Guidelines

  1. Layered Architecture: Always use for enterprise applications
  2. DTO Pattern: Use for all API request/response objects
  3. Repository Pattern: Use Spring Data JPA for data access
  4. Service Pattern: Implement business logic in service layer
  5. Strategy Pattern: Use for interchangeable algorithms
  6. Builder Pattern: Use for complex object construction
  7. Factory Pattern: Use for object creation logic
  8. Observer Pattern: Use for event-driven architecture
  9. Specification Pattern: Use for dynamic query building
  10. Caching Pattern: Use for frequently accessed data
  11. Async Pattern: Use for time-consuming operations
  12. Circuit Breaker: Use for external service calls

Anti-Patterns to Avoid

  • ❌ 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