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