Chapter 1: The Service Layer in Detail
Introduction
The Service Layer is the central element of the 3-layer architecture. It acts as a bridge between the controller (which handles HTTP requests) and the repository (which accesses data). This is where the business logic of your application resides.
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the role and responsibilities of the Service Layer
- Properly separate concerns between layers
- Implement a professional Service Layer
- Use dependency injection correctly
- Apply best practices
3-Layer Architecture
Overview
Responsibilities of Each Layer
| Layer | Responsibilities | Examples |
|---|---|---|
| Controller | HTTP Reception, Validation, Conversion, Responses | @RestController, @GetMapping |
| Service | Business logic, Orchestration, Transactions, Rule validation | @Service, Calculations |
| Repository | Data access, DB queries, Entity mapping | @Repository, JpaRepository |
What is Business Logic?
Definition
Business logic is the set of rules and processes that define the behavior of your application.
Examples of Business Logic
❌ NOT business logic:
// This is data access
productRepository.findById(id);
// This is HTTP handling
return ResponseEntity.ok(product);
✅ Business logic:
// Check stock before creating an order
if (product.getStock() < order.getQuantity()) {
throw new InsufficientStockException();
}
// Calculate total price with taxes
double totalPrice = order.getSubtotal() * (1 + TAX_RATE);
// Apply a promotion
if (customer.isPremium()) {
totalPrice = totalPrice * 0.9; // 10% discount
}
Creating a Service Layer
Basic Example
package com.example.demo.service;
import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
private final ProductRepository productRepository;
// Constructor injection (RECOMMENDED)
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// CREATE
public Product createProduct(Product product) {
// Business logic: validation
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
// Call to repository
return productRepository.save(product);
}
// READ - All
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// READ - By ID
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
}
// UPDATE
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);
// Business logic: validation
if (productDetails.getPrice() < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
// Update fields
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}
// DELETE
public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}
Key Points
- @Service: Marks the class as a Service Layer
- Constructor injection: Cleaner and more testable than @Autowired on fields
- Business validation: Check rules before data access
- Error handling: Throw appropriate exceptions
Dependency Injection
Three Injection Methods
1. Constructor Injection (RECOMMENDED)
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}
Advantages:
- ✅ Immutability (final)
- ✅ Easily testable
- ✅ Mandatory (compile error if missing)
- ✅ Recommended by Spring
2. Field Injection (NOT RECOMMENDED)
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
}
Disadvantages:
- ❌ Not easily testable
- ❌ Hidden dependencies
- ❌ Not immutable
3. Setter Injection (RARELY USED)
@Service
public class ProductService {
private ProductRepository productRepository;
@Autowired
public void setProductRepository(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}
Separation of Concerns
SOLID Principle: Single Responsibility
Each layer should have ONE single responsibility.
❌ Bad Example: Everything in the Controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductRepository productRepository;
@PostMapping
public Product create(@RequestBody Product product) {
// Business logic in the controller (BAD!)
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Invalid price");
}
// Direct access to repository (BAD!)
return productRepository.save(product);
}
}
Problems:
- ❌ Business logic in the controller
- ❌ Hard to test
- ❌ Hard to reuse
- ❌ Violation of single responsibility principle
✅ Good Example: Clear Separation
// Controller: HTTP handling only
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping
public Product create(@RequestBody Product product) {
return productService.createProduct(product);
}
}
// Service: Business logic
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product createProduct(Product product) {
// Business validation
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Invalid price");
}
// Call to repository
return productRepository.save(product);
}
}
// Repository: Data access
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Spring automatically generates the implementation
}
Advanced Example: Orchestration
The Service Layer can coordinate multiple repositories:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final CustomerRepository customerRepository;
public OrderService(OrderRepository orderRepository,
ProductRepository productRepository,
CustomerRepository customerRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.customerRepository = customerRepository;
}
public Order createOrder(Long customerId, Long productId, int quantity) {
// 1. Check that the customer exists
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new RuntimeException("Customer not found"));
// 2. Check that the product exists
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
// 3. Check stock
if (product.getStock() < quantity) {
throw new InsufficientStockException("Insufficient stock");
}
// 4. Calculate total price
double totalPrice = product.getPrice() * quantity;
// 5. Apply discount if customer is premium
if (customer.isPremium()) {
totalPrice = totalPrice * 0.9;
}
// 6. Create the order
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
order.setQuantity(quantity);
order.setTotalPrice(totalPrice);
// 7. Decrease stock
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// 8. Save the order
return orderRepository.save(order);
}
}
Transaction Management
@Transactional Annotation
@Service
public class OrderService {
@Transactional
public Order createOrder(Long customerId, Long productId, int quantity) {
// All operations are within a transaction
// If an exception is thrown, everything is rolled back
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.setStock(product.getStock() - quantity);
productRepository.save(product);
Order order = new Order();
// ... create order
return orderRepository.save(order);
}
}
Advantages:
- ✅ Data consistency
- ✅ Automatic rollback in case of error
- ✅ ACID (Atomicity, Consistency, Isolation, Durability)
Best Practices
DO
✅ Constructor injection
public ProductService(ProductRepository repository) {
this.repository = repository;
}
✅ Methods with explicit names
public Product createProduct(Product product)
public void activateUser(Long userId)
public double calculateTotalPrice(Order order)
✅ Business validation in the Service
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Invalid price");
}
✅ Appropriate exception handling
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
DON'T
❌ Business logic in the Controller
// BAD: In the controller
@PostMapping
public Product create(@RequestBody Product product) {
if (product.getPrice() < 0) { // Business logic!
throw new Exception();
}
return service.create(product);
}
❌ Direct Repository access from the Controller
// BAD
@RestController
public class ProductController {
@Autowired
private ProductRepository repository; // Direct!
}
❌ Field injection
// BAD
@Autowired
private ProductRepository repository;
Practical Exercises
Exercise 1: Basic Service
Objective: Create a complete Service Layer
Instructions:
- Create a
Studententity:- Long id
- String firstName
- String lastName
- String email
- int grade
- Create
StudentRepository - Create
StudentServicewith:- All CRUD operations
- Validation: grade between 0 and 100
- Validation: valid email
- Create
StudentControllerusing the service
Exercise 2: Orchestration
Objective: Coordinate multiple repositories
Instructions:
- Create
Library(id, name, address) - Create
Book(id, title, author, libraryId) - Create
LibraryServicewith a method:public Library createLibraryWithBooks(Library library, List<Book> books)- Create the library
- Create all associated books
- All within a transaction
Chapter Summary
Key Points
- The Service Layer contains the business logic
- Separation of concerns: Controller → Service → Repository
- Constructor injection (recommended)
- @Transactional for data consistency
- Validation in the Service, not in the Controller
What You Now Know
✅ Role and responsibilities of the Service Layer
✅ Separation of concerns
✅ Dependency injection
✅ Orchestration of multiple repositories
✅ Professional best practices