Application Service Layer in Java

The Application Service Layer orchestrates business use cases, coordinates domain objects, and handles cross-cutting concerns like transactions and security. It sits between the presentation layer and domain layer.

Basic Application Service Structure

1. Service Interfaces and DTOs

// Common base interface for application services public interface ApplicationService { // Marker interface } // Result pattern for service responses public class ServiceResult<T> { private final boolean success; private final T data; private final String error; private final List<ValidationError> validationErrors; private ServiceResult(boolean success, T data, String error, List<ValidationError> validationErrors) { this.success = success; this.data = data; this.error = error; this.validationErrors = validationErrors != null ? new ArrayList<>(validationErrors) : new ArrayList<>(); } // Factory methods public static <T> ServiceResult<T> success(T data) { return new ServiceResult<>(true, data, null, null); } public static <T> ServiceResult<T> error(String error) { return new ServiceResult<>(false, null, error, null); } public static <T> ServiceResult<T> validationError(List<ValidationError> errors) { return new ServiceResult<>(false, null, "Validation failed", errors); } // Getters public boolean isSuccess() { return success; } public T getData() { return data; } public String getError() { return error; } public List<ValidationError> getValidationErrors() { return Collections.unmodifiableList(validationErrors); } } // Validation error details public class ValidationError { private final String field; private final String message; private final String code; public ValidationError(String field, String message, String code) { this.field = field; this.message = message; this.code = code; } // Getters public String getField() { return field; } public String getMessage() { return message; } public String getCode() { return code; } } // Pagination support public class PaginatedResult<T> { private final List<T> items; private final int totalPages; private final long totalItems; private final int currentPage; private final int pageSize; public PaginatedResult(List<T> items, int totalPages, long totalItems, int currentPage, int pageSize) { this.items = new ArrayList<>(items); this.totalPages = totalPages; this.totalItems = totalItems; this.currentPage = currentPage; this.pageSize = pageSize; } // Getters public List<T> getItems() { return Collections.unmodifiableList(items); } public int getTotalPages() { return totalPages; } public long getTotalItems() { return totalItems; } public int getCurrentPage() { return currentPage; } public int getPageSize() { return pageSize; } public boolean hasNext() { return currentPage < totalPages; } public boolean hasPrevious() { return currentPage > 1; } }

2. Customer Management Service

// DTOs for customer operations public class CreateCustomerRequest { private String email; private String name; private String street; private String city; private String state; private String zipCode; private String country; // Constructors, getters, and setters public CreateCustomerRequest() {} public CreateCustomerRequest(String email, String name, String street, String city, String state, String zipCode, String country) { this.email = email; this.name = name; this.street = street; this.city = city; this.state = state; this.zipCode = zipCode; this.country = country; } // Getters and setters public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getZipCode() { return zipCode; } public void setZipCode(String zipCode) { this.zipCode = zipCode; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } } public class CustomerResponse { private final UUID id; private final String email; private final String name; private final String status; private final String address; private final LocalDateTime createdAt; private final double totalSpent; public CustomerResponse(UUID id, String email, String name, String status, String address, LocalDateTime createdAt, double totalSpent) { this.id = id; this.email = email; this.name = name; this.status = status; this.address = address; this.createdAt = createdAt; this.totalSpent = totalSpent; } // Getters public UUID getId() { return id; } public String getEmail() { return email; } public String getName() { return name; } public String getStatus() { return status; } public String getAddress() { return address; } public LocalDateTime getCreatedAt() { return createdAt; } public double getTotalSpent() { return totalSpent; } } // Customer service interface public interface CustomerService extends ApplicationService { ServiceResult<CustomerResponse> createCustomer(CreateCustomerRequest request); ServiceResult<CustomerResponse> getCustomer(UUID customerId); ServiceResult<CustomerResponse> updateCustomer(UUID customerId, UpdateCustomerRequest request); ServiceResult<Void> deactivateCustomer(UUID customerId); ServiceResult<Void> activateCustomer(UUID customerId); ServiceResult<PaginatedResult<CustomerResponse>> searchCustomers(CustomerSearchCriteria criteria); ServiceResult<List<OrderResponse>> getCustomerOrders(UUID customerId); } // Search criteria public class CustomerSearchCriteria { private String email; private String name; private String status; private LocalDate createdAfter; private LocalDate createdBefore; private int page = 1; private int size = 20; private String sortBy = "createdAt"; private String sortDirection = "DESC"; // Constructors, getters, and setters public CustomerSearchCriteria() {} // Getters and setters public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public LocalDate getCreatedAfter() { return createdAfter; } public void setCreatedAfter(LocalDate createdAfter) { this.createdAfter = createdAfter; } public LocalDate getCreatedBefore() { return createdBefore; } public void setCreatedBefore(LocalDate createdBefore) { this.createdBefore = createdBefore; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } public String getSortBy() { return sortBy; } public void setSortBy(String sortBy) { this.sortBy = sortBy; } public String getSortDirection() { return sortDirection; } public void setSortDirection(String sortDirection) { this.sortDirection = sortDirection; } }

3. Customer Service Implementation

import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Service; @Service @Transactional public class CustomerServiceImpl implements CustomerService { private final CustomerRepository customerRepository; private final OrderRepository orderRepository; private final EmailService emailService; private final EventPublisher eventPublisher; public CustomerServiceImpl(CustomerRepository customerRepository, OrderRepository orderRepository, EmailService emailService, EventPublisher eventPublisher) { this.customerRepository = customerRepository; this.orderRepository = orderRepository; this.emailService = emailService; this.eventPublisher = eventPublisher; } @Override public ServiceResult<CustomerResponse> createCustomer(CreateCustomerRequest request) { try { // Validate request List<ValidationError> validationErrors = validateCreateRequest(request); if (!validationErrors.isEmpty()) { return ServiceResult.validationError(validationErrors); } // Check for duplicate email if (customerRepository.existsByEmail(request.getEmail())) { return ServiceResult.error("Customer with this email already exists"); } // Create address value object Address address = Address.of( request.getStreet(), request.getCity(), request.getState(), request.getZipCode(), request.getCountry() ); // Create customer entity Customer customer = new Customer(request.getEmail(), request.getName(), address); // Save customer Customer savedCustomer = customerRepository.save(customer); // Send welcome email (fire and forget) emailService.sendWelcomeEmail(savedCustomer.getEmail(), savedCustomer.getName()); // Publish domain event eventPublisher.publish(new CustomerCreatedEvent(savedCustomer.getId(), savedCustomer.getEmail())); // Return response CustomerResponse response = mapToCustomerResponse(savedCustomer); return ServiceResult.success(response); } catch (Exception e) { // Log error System.err.println("Error creating customer: " + e.getMessage()); return ServiceResult.error("Failed to create customer: " + e.getMessage()); } } @Override public ServiceResult<CustomerResponse> getCustomer(UUID customerId) { try { Customer customer = customerRepository.findById(customerId) .orElse(null); if (customer == null) { return ServiceResult.error("Customer not found"); } CustomerResponse response = mapToCustomerResponse(customer); return ServiceResult.success(response); } catch (Exception e) { System.err.println("Error retrieving customer: " + e.getMessage()); return ServiceResult.error("Failed to retrieve customer"); } } @Override public ServiceResult<CustomerResponse> updateCustomer(UUID customerId, UpdateCustomerRequest request) { try { Customer customer = customerRepository.findById(customerId) .orElse(null); if (customer == null) { return ServiceResult.error("Customer not found"); } // Validate request List<ValidationError> validationErrors = validateUpdateRequest(request); if (!validationErrors.isEmpty()) { return ServiceResult.validationError(validationErrors); } // Update customer Address newAddress = Address.of( request.getStreet(), request.getCity(), request.getState(), request.getZipCode(), request.getCountry() ); customer.updateProfile(request.getName(), newAddress); // Save changes Customer updatedCustomer = customerRepository.save(customer); // Publish event eventPublisher.publish(new CustomerUpdatedEvent(customerId)); CustomerResponse response = mapToCustomerResponse(updatedCustomer); return ServiceResult.success(response); } catch (Exception e) { System.err.println("Error updating customer: " + e.getMessage()); return ServiceResult.error("Failed to update customer"); } } @Override public ServiceResult<Void> deactivateCustomer(UUID customerId) { try { Customer customer = customerRepository.findById(customerId) .orElse(null); if (customer == null) { return ServiceResult.error("Customer not found"); } customer.deactivate(); customerRepository.save(customer); eventPublisher.publish(new CustomerDeactivatedEvent(customerId)); return ServiceResult.success(null); } catch (Exception e) { System.err.println("Error deactivating customer: " + e.getMessage()); return ServiceResult.error("Failed to deactivate customer"); } } @Override public ServiceResult<PaginatedResult<CustomerResponse>> searchCustomers(CustomerSearchCriteria criteria) { try { // Convert criteria to domain specification CustomerSpecification spec = new CustomerSpecification(criteria); // Get paginated results Page<Customer> customerPage = customerRepository.findAll(spec, PageRequest.of(criteria.getPage() - 1, criteria.getSize(), getSort(criteria.getSortBy(), criteria.getSortDirection()))); // Map to responses List<CustomerResponse> responses = customerPage.getContent().stream() .map(this::mapToCustomerResponse) .collect(Collectors.toList()); PaginatedResult<CustomerResponse> result = new PaginatedResult<>( responses, customerPage.getTotalPages(), customerPage.getTotalElements(), criteria.getPage(), criteria.getSize() ); return ServiceResult.success(result); } catch (Exception e) { System.err.println("Error searching customers: " + e.getMessage()); return ServiceResult.error("Failed to search customers"); } } @Override public ServiceResult<List<OrderResponse>> getCustomerOrders(UUID customerId) { try { if (!customerRepository.existsById(customerId)) { return ServiceResult.error("Customer not found"); } List<Order> orders = orderRepository.findByCustomerId(customerId); List<OrderResponse> responses = orders.stream() .map(this::mapToOrderResponse) .collect(Collectors.toList()); return ServiceResult.success(responses); } catch (Exception e) { System.err.println("Error retrieving customer orders: " + e.getMessage()); return ServiceResult.error("Failed to retrieve customer orders"); } } // Helper methods private List<ValidationError> validateCreateRequest(CreateCustomerRequest request) { List<ValidationError> errors = new ArrayList<>(); if (request.getEmail() == null || request.getEmail().trim().isEmpty()) { errors.add(new ValidationError("email", "Email is required", "REQUIRED")); } else if (!isValidEmail(request.getEmail())) { errors.add(new ValidationError("email", "Invalid email format", "INVALID_FORMAT")); } if (request.getName() == null || request.getName().trim().isEmpty()) { errors.add(new ValidationError("name", "Name is required", "REQUIRED")); } else if (request.getName().trim().length() < 2) { errors.add(new ValidationError("name", "Name must be at least 2 characters", "MIN_LENGTH")); } // Add more validations as needed return errors; } private List<ValidationError> validateUpdateRequest(UpdateCustomerRequest request) { List<ValidationError> errors = new ArrayList<>(); if (request.getName() != null && request.getName().trim().length() < 2) { errors.add(new ValidationError("name", "Name must be at least 2 characters", "MIN_LENGTH")); } return errors; } private boolean isValidEmail(String email) { return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$"); } private Sort getSort(String sortBy, String sortDirection) { Sort.Direction direction = "DESC".equalsIgnoreCase(sortDirection) ? Sort.Direction.DESC : Sort.Direction.ASC; return Sort.by(direction, sortBy); } private CustomerResponse mapToCustomerResponse(Customer customer) { return new CustomerResponse( customer.getId(), customer.getEmail(), customer.getName(), customer.getStatus().name(), customer.getAddress().getFormattedAddress(), customer.getCreatedAt(), customer.getTotalSpent() ); } private OrderResponse mapToOrderResponse(Order order) { return new OrderResponse( order.getId(), order.getStatus().name(), order.getTotalAmount().getAmount(), order.getCreatedAt() ); } }

Order Management Service

4. Order Service Implementation

// Order DTOs public class CreateOrderRequest { private UUID customerId; private List<OrderItemRequest> items; private String shippingInstructions; // Constructors, getters, setters public CreateOrderRequest() {} public CreateOrderRequest(UUID customerId, List<OrderItemRequest> items) { this.customerId = customerId; this.items = items; } public UUID getCustomerId() { return customerId; } public void setCustomerId(UUID customerId) { this.customerId = customerId; } public List<OrderItemRequest> getItems() { return items; } public void setItems(List<OrderItemRequest> items) { this.items = items; } public String getShippingInstructions() { return shippingInstructions; } public void setShippingInstructions(String shippingInstructions) { this.shippingInstructions = shippingInstructions; } } public class OrderItemRequest { private UUID productId; private int quantity; // Constructors, getters, setters public OrderItemRequest() {} public OrderItemRequest(UUID productId, int quantity) { this.productId = productId; this.quantity = quantity; } public UUID getProductId() { return productId; } public void setProductId(UUID productId) { this.productId = productId; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } } public class OrderResponse { private final UUID id; private final UUID customerId; private final String status; private final BigDecimal totalAmount; private final LocalDateTime createdAt; private final List<OrderItemResponse> items; public OrderResponse(UUID id, UUID customerId, String status, BigDecimal totalAmount, LocalDateTime createdAt, List<OrderItemResponse> items) { this.id = id; this.customerId = customerId; this.status = status; this.totalAmount = totalAmount; this.createdAt = createdAt; this.items = items; } // Getters public UUID getId() { return id; } public UUID getCustomerId() { return customerId; } public String getStatus() { return status; } public BigDecimal getTotalAmount() { return totalAmount; } public LocalDateTime getCreatedAt() { return createdAt; } public List<OrderItemResponse> getItems() { return items; } } // Order service interface public interface OrderService extends ApplicationService { ServiceResult<OrderResponse> createOrder(CreateOrderRequest request); ServiceResult<OrderResponse> getOrder(UUID orderId); ServiceResult<OrderResponse> confirmOrder(UUID orderId); ServiceResult<OrderResponse> cancelOrder(UUID orderId); ServiceResult<OrderResponse> completeOrder(UUID orderId); ServiceResult<PaginatedResult<OrderResponse>> getOrders(OrderSearchCriteria criteria); } @Service @Transactional public class OrderServiceImpl implements OrderService { private final OrderRepository orderRepository; private final CustomerRepository customerRepository; private final ProductRepository productRepository; private final InventoryService inventoryService; private final PaymentService paymentService; private final EventPublisher eventPublisher; private final DomainMapper domainMapper; public OrderServiceImpl(OrderRepository orderRepository, CustomerRepository customerRepository, ProductRepository productRepository, InventoryService inventoryService, PaymentService paymentService, EventPublisher eventPublisher, DomainMapper domainMapper) { this.orderRepository = orderRepository; this.customerRepository = customerRepository; this.productRepository = productRepository; this.inventoryService = inventoryService; this.paymentService = paymentService; this.eventPublisher = eventPublisher; this.domainMapper = domainMapper; } @Override public ServiceResult<OrderResponse> createOrder(CreateOrderRequest request) { try { // Validate request List<ValidationError> validationErrors = validateOrderRequest(request); if (!validationErrors.isEmpty()) { return ServiceResult.validationError(validationErrors); } // Find customer Customer customer = customerRepository.findById(request.getCustomerId()) .orElse(null); if (customer == null) { return ServiceResult.error("Customer not found"); } // Check if customer can place orders if (!customer.canPlaceOrder()) { return ServiceResult.error("Customer cannot place orders at this time"); } // Process order items Set<OrderItem> orderItems = processOrderItems(request.getItems()); if (orderItems.isEmpty()) { return ServiceResult.error("No valid order items"); } // Create order Order order = new Order(customer, orderItems); // Apply shipping instructions if provided if (request.getShippingInstructions() != null) { ShippingAddress shippingAddress = order.getShippingAddress() .withInstructions(request.getShippingInstructions()); order.updateShippingAddress(shippingAddress); } // Save order Order savedOrder = orderRepository.save(order); // Reserve inventory inventoryService.reserveInventory(savedOrder.getId(), request.getItems()); // Publish event eventPublisher.publish(new OrderCreatedEvent(savedOrder.getId(), customer.getId())); // Map to response OrderResponse response = domainMapper.toOrderResponse(savedOrder); return ServiceResult.success(response); } catch (Exception e) { System.err.println("Error creating order: " + e.getMessage()); return ServiceResult.error("Failed to create order: " + e.getMessage()); } } @Override public ServiceResult<OrderResponse> confirmOrder(UUID orderId) { try { Order order = orderRepository.findById(orderId) .orElse(null); if (order == null) { return ServiceResult.error("Order not found"); } // Process payment PaymentResult paymentResult = paymentService.processPayment(orderId, order.getTotalAmount()); if (!paymentResult.isSuccess()) { return ServiceResult.error("Payment failed: " + paymentResult.getErrorMessage()); } // Confirm order order.confirm(); Order updatedOrder = orderRepository.save(order); // Update inventory inventoryService.allocateInventory(orderId); // Publish event eventPublisher.publish(new OrderConfirmedEvent(orderId)); OrderResponse response = domainMapper.toOrderResponse(updatedOrder); return ServiceResult.success(response); } catch (Exception e) { System.err.println("Error confirming order: " + e.getMessage()); return ServiceResult.error("Failed to confirm order"); } } @Override public ServiceResult<OrderResponse> cancelOrder(UUID orderId) { try { Order order = orderRepository.findById(orderId) .orElse(null); if (order == null) { return ServiceResult.error("Order not found"); } // Cancel order order.cancel(); Order updatedOrder = orderRepository.save(order); // Release inventory inventoryService.releaseInventory(orderId); // Refund payment if already processed if (order.getStatus() == OrderStatus.CONFIRMED) { paymentService.refundPayment(orderId); } // Publish event eventPublisher.publish(new OrderCancelledEvent(orderId)); OrderResponse response = domainMapper.toOrderResponse(updatedOrder); return ServiceResult.success(response); } catch (Exception e) { System.err.println("Error cancelling order: " + e.getMessage()); return ServiceResult.error("Failed to cancel order"); } } // Helper methods private Set<OrderItem> processOrderItems(List<OrderItemRequest> itemRequests) { Set<OrderItem> orderItems = new HashSet<>(); for (OrderItemRequest itemRequest : itemRequests) { Product product = productRepository.findById(itemRequest.getProductId()) .orElse(null); if (product == null || !product.isAvailable()) { continue; // Skip unavailable products } if (!product.canFulfillOrder(itemRequest.getQuantity())) { continue; // Skip if insufficient stock } OrderItem orderItem = new OrderItem( product.getId(), product.getName(), product.getPrice(), itemRequest.getQuantity() ); orderItems.add(orderItem); } return orderItems; } private List<ValidationError> validateOrderRequest(CreateOrderRequest request) { List<ValidationError> errors = new ArrayList<>(); if (request.getCustomerId() == null) { errors.add(new ValidationError("customerId", "Customer ID is required", "REQUIRED")); } if (request.getItems() == null || request.getItems().isEmpty()) { errors.add(new ValidationError("items", "At least one order item is required", "REQUIRED")); } else { for (int i = 0; i < request.getItems().size(); i++) { OrderItemRequest item = request.getItems().get(i); if (item.getProductId() == null) { errors.add(new ValidationError("items[" + i + "].productId", "Product ID is required", "REQUIRED")); } if (item.getQuantity() <= 0) { errors.add(new ValidationError("items[" + i + "].quantity", "Quantity must be positive", "INVALID")); } } } return errors; } }

Advanced Service Patterns

5. CQRS with Query Services

// Query service for read operations public interface CustomerQueryService extends ApplicationService { ServiceResult<CustomerDetailResponse> getCustomerDetail(UUID customerId); ServiceResult<PaginatedResult<CustomerSummaryResponse>> getCustomerSummaries(CustomerSearchCriteria criteria); ServiceResult<CustomerStatisticsResponse> getCustomerStatistics(UUID customerId); } @Service @Transactional(readOnly = true) public class CustomerQueryServiceImpl implements CustomerQueryService { private final CustomerReadRepository customerReadRepository; private final OrderReadRepository orderReadRepository; public CustomerQueryServiceImpl(CustomerReadRepository customerReadRepository, OrderReadRepository orderReadRepository) { this.customerReadRepository = customerReadRepository; this.orderReadRepository = orderReadRepository; } @Override public ServiceResult<CustomerDetailResponse> getCustomerDetail(UUID customerId) { try { CustomerDetailProjection customerDetail = customerReadRepository.findDetailById(customerId); if (customerDetail == null) { return ServiceResult.error("Customer not found"); } List<OrderSummaryProjection> recentOrders = orderReadRepository.findRecentOrdersByCustomerId(customerId, 10); CustomerDetailResponse response = mapToDetailResponse(customerDetail, recentOrders); return ServiceResult.success(response); } catch (Exception e) { System.err.println("Error retrieving customer detail: " + e.getMessage()); return ServiceResult.error("Failed to retrieve customer details"); } } @Override public ServiceResult<PaginatedResult<CustomerSummaryResponse>> getCustomerSummaries(CustomerSearchCriteria criteria) { try { Page<CustomerSummaryProjection> summaryPage = customerReadRepository.findSummaries( criteria.getEmail(), criteria.getName(), criteria.getStatus(), PageRequest.of(criteria.getPage() - 1, criteria.getSize()) ); List<CustomerSummaryResponse> responses = summaryPage.getContent().stream() .map(this::mapToSummaryResponse) .collect(Collectors.toList()); PaginatedResult<CustomerSummaryResponse> result = new PaginatedResult<>( responses, summaryPage.getTotalPages(), summaryPage.getTotalElements(), criteria.getPage(), criteria.getSize() ); return ServiceResult.success(result); } catch (Exception e) { System.err.println("Error retrieving customer summaries: " + e.getMessage()); return ServiceResult.error("Failed to retrieve customer summaries"); } } @Override public ServiceResult<CustomerStatisticsResponse> getCustomerStatistics(UUID customerId) { try { CustomerStatistics stats = customerReadRepository.findStatisticsById(customerId); if (stats == null) { return ServiceResult.error("Customer not found"); } CustomerStatisticsResponse response = new CustomerStatisticsResponse( stats.getTotalOrders(), stats.getTotalSpent(), stats.getAverageOrderValue(), stats.getLastOrderDate(), stats.getFavoriteCategory() ); return ServiceResult.success(response); } catch (Exception e) { System.err.println("Error retrieving customer statistics: " + e.getMessage()); return ServiceResult.error("Failed to retrieve customer statistics"); } } // Mapping methods private CustomerDetailResponse mapToDetailResponse(CustomerDetailProjection detail, List<OrderSummaryProjection> recentOrders) { return new CustomerDetailResponse( detail.getId(), detail.getEmail(), detail.getName(), detail.getStatus(), detail.getAddress(), detail.getCreatedAt(), detail.getTotalSpent(), recentOrders.stream() .map(this::mapToOrderSummaryResponse) .collect(Collectors.toList()) ); } private CustomerSummaryResponse mapToSummaryResponse(CustomerSummaryProjection summary) { return new CustomerSummaryResponse( summary.getId(), summary.getEmail(), summary.getName(), summary.getStatus(), summary.getTotalOrders(), summary.getTotalSpent(), summary.getLastOrderDate() ); } private OrderSummaryResponse mapToOrderSummaryResponse(OrderSummaryProjection order) { return new OrderSummaryResponse( order.getId(), order.getStatus(), order.getTotalAmount(), order.getCreatedAt() ); } }

6. Service Decorators for Cross-Cutting Concerns

// Logging decorator @Component @Primary public class LoggingCustomerServiceDecorator implements CustomerService { private final CustomerService delegate; private final Logger logger = LoggerFactory.getLogger(LoggingCustomerServiceDecorator.class); public LoggingCustomerServiceDecorator(CustomerService delegate) { this.delegate = delegate; } @Override public ServiceResult<CustomerResponse> createCustomer(CreateCustomerRequest request) { logger.info("Creating customer with email: {}", request.getEmail()); long startTime = System.currentTimeMillis(); try { ServiceResult<CustomerResponse> result = delegate.createCustomer(request); long duration = System.currentTimeMillis() - startTime; if (result.isSuccess()) { logger.info("Customer created successfully in {} ms", duration); } else { logger.warn("Customer creation failed in {} ms: {}", duration, result.getError()); } return result; } catch (Exception e) { logger.error("Unexpected error creating customer", e); throw e; } } // Implement other methods with similar logging... } // Caching decorator @Component @Order(1) public class CachingCustomerServiceDecorator implements CustomerService { private final CustomerService delegate; private final CacheManager cacheManager; public CachingCustomerServiceDecorator(CustomerService delegate, CacheManager cacheManager) { this.delegate = delegate; this.cacheManager = cacheManager; } @Override public ServiceResult<CustomerResponse> getCustomer(UUID customerId) { String cacheKey = "customer:" + customerId; Cache cache = cacheManager.getCache("customers"); // Try cache first CustomerResponse cached = cache.get(cacheKey, CustomerResponse.class); if (cached != null) { return ServiceResult.success(cached); } // Delegate to actual service ServiceResult<CustomerResponse> result = delegate.getCustomer(customerId); // Cache successful results if (result.isSuccess()) { cache.put(cacheKey, result.getData()); } return result; } @Override public ServiceResult<CustomerResponse> updateCustomer(UUID customerId, UpdateCustomerRequest request) { ServiceResult<CustomerResponse> result = delegate.updateCustomer(customerId, request); // Evict cache on update if (result.isSuccess()) { Cache cache = cacheManager.getCache("customers"); cache.evict("customer:" + customerId); } return result; } // Implement other methods... } // Validation decorator @Component @Order(2) public class ValidationCustomerServiceDecorator implements CustomerService { private final CustomerService delegate; private final Validator validator; public ValidationCustomerServiceDecorator(CustomerService delegate, Validator validator) { this.delegate = delegate; this.validator = validator; } @Override public ServiceResult<CustomerResponse> createCustomer(CreateCustomerRequest request) { // Perform JSR-303 validation Set<ConstraintViolation<CreateCustomerRequest>> violations = validator.validate(request); if (!violations.isEmpty()) { List<ValidationError> errors = violations.stream() .map(v -> new ValidationError( v.getPropertyPath().toString(), v.getMessage(), v.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName() )) .collect(Collectors.toList()); return ServiceResult.validationError(errors); } return delegate.createCustomer(request); } // Implement other methods... }

Service Configuration and Testing

7. Service Configuration

@Configuration public class ServiceConfiguration { @Bean @Primary public CustomerService customerService(CustomerRepository customerRepository, OrderRepository orderRepository, EmailService emailService, EventPublisher eventPublisher) { CustomerService coreService = new CustomerServiceImpl( customerRepository, orderRepository, emailService, eventPublisher); // Wrap with decorators return new ValidationCustomerServiceDecorator( new CachingCustomerServiceDecorator( new LoggingCustomerServiceDecorator(coreService), cacheManager() ), validator() ); } @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("customers", "orders"); } @Bean public Validator validator() { return Validation.buildDefaultValidatorFactory().getValidator(); } }

8. Service Testing

@ExtendWith(MockitoExtension.class) class CustomerServiceImplTest { @Mock private CustomerRepository customerRepository; @Mock private OrderRepository orderRepository; @Mock private EmailService emailService; @Mock private EventPublisher eventPublisher; @InjectMocks private CustomerServiceImpl customerService; @Test void createCustomer_WithValidRequest_ShouldReturnSuccess() { // Arrange CreateCustomerRequest request = new CreateCustomerRequest( "[email protected]", "John Doe", "123 Main St", "Springfield", "IL", "62701", "US" ); when(customerRepository.existsByEmail("[email protected]")).thenReturn(false); when(customerRepository.save(any(Customer.class))).thenAnswer(invocation -> { Customer customer = invocation.getArgument(0); return customer; // Simulate saved entity }); // Act ServiceResult<CustomerResponse> result = customerService.createCustomer(request); // Assert assertTrue(result.isSuccess()); assertNotNull(result.getData()); assertEquals("[email protected]", result.getData().getEmail()); verify(customerRepository).save(any(Customer.class)); verify(emailService).sendWelcomeEmail(eq("[email protected]"), anyString()); verify(eventPublisher).publish(any(CustomerCreatedEvent.class)); } @Test void createCustomer_WithDuplicateEmail_ShouldReturnError() { // Arrange CreateCustomerRequest request = new CreateCustomerRequest( "[email protected]", "John Doe", "123 Main St", "Springfield", "IL", "62701", "US" ); when(customerRepository.existsByEmail("[email protected]")).thenReturn(true); // Act ServiceResult<CustomerResponse> result = customerService.createCustomer(request); // Assert assertFalse(result.isSuccess()); assertEquals("Customer with this email already exists", result.getError()); verify(customerRepository, never()).save(any(Customer.class)); } @Test void getCustomer_WithExistingId_ShouldReturnCustomer() { // Arrange UUID customerId = UUID.randomUUID(); Customer customer = createTestCustomer(customerId); when(customerRepository.findById(customerId)).thenReturn(Optional.of(customer)); // Act ServiceResult<CustomerResponse> result = customerService.getCustomer(customerId); // Assert assertTrue(result.isSuccess()); assertEquals(customerId, result.getData().getId()); assertEquals("[email protected]", result.getData().getEmail()); } @Test void getCustomer_WithNonExistingId_ShouldReturnError() { // Arrange UUID customerId = UUID.randomUUID(); when(customerRepository.findById(customerId)).thenReturn(Optional.empty()); // Act ServiceResult<CustomerResponse> result = customerService.getCustomer(customerId); // Assert assertFalse(result.isSuccess()); assertEquals("Customer not found", result.getError()); } private Customer createTestCustomer(UUID id) { Address address = Address.of("123 Main St", "Springfield", "IL", "62701", "US"); return new Customer(id, "[email protected]", "John Doe", address, CustomerStatus.ACTIVE, new HashSet<>(), LocalDateTime.now(), LocalDateTime.now()); } }

Key Benefits of Application Service Layer

  1. Use Case Orchestration: Coordinates multiple domain objects to fulfill business use cases
  2. Transaction Management: Handles transaction boundaries appropriately
  3. Cross-Cutting Concerns: Centralizes security, logging, validation, and caching
  4. Presentation Layer Abstraction: Shields presentation layer from domain complexity
  5. Testability: Services can be easily unit tested with mocked dependencies
  6. Command-Query Separation: Separate services for commands and queries
  7. Error Handling: Consistent error handling and response patterns

The Application Service Layer provides a clean, maintainable way to implement business use cases while keeping the domain model focused on business logic and rules.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper