Skip to main content

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​

LayerResponsibilitiesExamples
ControllerHTTP Reception, Validation, Conversion, Responses@RestController, @GetMapping
ServiceBusiness logic, Orchestration, Transactions, Rule validation@Service, Calculations
RepositoryData 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​

  1. @Service: Marks the class as a Service Layer
  2. Constructor injection: Cleaner and more testable than @Autowired on fields
  3. Business validation: Check rules before data access
  4. Error handling: Throw appropriate exceptions

Dependency Injection​

Three Injection Methods​

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

  1. Create a Student entity:
    • Long id
    • String firstName
    • String lastName
    • String email
    • int grade
  2. Create StudentRepository
  3. Create StudentService with:
    • All CRUD operations
    • Validation: grade between 0 and 100
    • Validation: valid email
  4. Create StudentController using the service

Exercise 2: Orchestration​

Objective: Coordinate multiple repositories

Instructions:

  1. Create Library (id, name, address)
  2. Create Book (id, title, author, libraryId)
  3. Create LibraryService with 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


Next Chapter: Data Validation

Back to Module 3