Skip to main content

Chapter 4: Professional 3-Layer Architecture

Introduction​

This chapter synthesizes everything you have learned in the previous chapters to create a professional, robust, and maintainable architecture.


Architecture Overview​


Responsibilities by Layer​

LayerRoleTechnologiesAnnotations
ControllerHTTP Management, Validation, ConversionSpring MVC@RestController, @GetMapping
ServiceBusiness Logic, Orchestration, TransactionsSpring Core@Service, @Transactional
RepositoryData Access, DB QueriesSpring Data JPA@Repository, JpaRepository
Exception HandlerError Handling, Response FormattingSpring MVC@ControllerAdvice

Complete Project: Product Management​

Package Structure​

src/main/java/com/example/demo/
├── DemoApplication.java
├── controller/
│ └── ProductController.java
├── service/
│ └── ProductService.java
├── repository/
│ └── ProductRepository.java
├── model/
│ └── Product.java
└── exception/
├── ProductNotFoundException.java
├── InvalidProductException.java
├── ErrorResponse.java
└── GlobalExceptionHandler.java

1. Entity (Model)​

package com.example.demo.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.*;

@Entity
@Table(name = "products")
public class Product {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank(message = "Name cannot be empty")
@Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
private String name;

@NotNull(message = "Price is required")
@Min(value = 0, message = "Price cannot be negative")
private Double price;

@NotNull(message = "Quantity is required")
@Min(value = 0, message = "Quantity cannot be negative")
private Integer quantity;

// Constructors
public Product() {}

public Product(String name, Double price, Integer quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}

// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }

public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
}

2. Repository​

package com.example.demo.repository;

import com.example.demo.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Inherited methods: save, findById, findAll, deleteById

// Custom methods
List<Product> findByName(String name);
List<Product> findByPriceLessThan(Double price);
}

3. Service​

package com.example.demo.service;

import com.example.demo.exception.ProductNotFoundException;
import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional
public class ProductService {

private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

// CREATE
public Product createProduct(Product product) {
// Additional business validation if necessary
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 ProductNotFoundException(id));
}

// UPDATE
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);

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

4. Controller​

package com.example.demo.controller;

import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

// CREATE
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

// READ - All
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return ResponseEntity.ok(products);
}

// READ - By ID
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}

// UPDATE
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}

// DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}

5. Exceptions​

package com.example.demo.exception;

public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}
package com.example.demo.exception;

import java.time.LocalDateTime;

public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;

public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}

// Getters and Setters (generated)
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }

public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }

public String getError() { return error; }
public void setError(String error) { this.error = error; }

public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }

public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
}

6. Exception Handler​

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {

ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {

Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex, WebRequest request) {

ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}

Configuration application.yml​

spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:

h2:
console:
enabled: true
path: /h2-console

jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true

server:
port: 8080

Testing with Postman​

1. CREATE - POST /api/products​

Request:

POST http://localhost:8080/api/products
Content-Type: application/json

{
"name": "Laptop",
"price": 999.99,
"quantity": 10
}

Response: 201 Created

{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}

2. GET ALL - GET /api/products​

Response: 200 OK

[
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}
]

3. GET BY ID - GET /api/products/1​

Response: 200 OK

{
"id": 1,
"name": "Laptop",
"price": 999.99,
"quantity": 10
}

4. UPDATE - PUT /api/products/1​

Request:

{
"name": "Laptop Pro",
"price": 1299.99,
"quantity": 5
}

Response: 200 OK

{
"id": 1,
"name": "Laptop Pro",
"price": 1299.99,
"quantity": 5
}

5. DELETE - DELETE /api/products/1​

Response: 204 No Content


Naming Conventions​

Packages​

com.example.demo.controller
com.example.demo.service
com.example.demo.repository
com.example.demo.model
com.example.demo.exception

Classes​

  • Controller: ProductController, UserController
  • Service: ProductService, UserService
  • Repository: ProductRepository, UserRepository
  • Model: Product, User
  • Exception: ProductNotFoundException, InvalidProductException

Methods​

  • CREATE: create...() or add...()
  • READ: get...(), find...(), getAll...()
  • UPDATE: update...()
  • DELETE: delete...()

Chapter Summary​

Professional Architecture​

✅ Controller: Only HTTP management
✅ Service: Business logic and orchestration
✅ Repository: Data access
✅ Exception Handler: Global error handling

Key Points​

  • Clear separation of responsibilities
  • Constructor injection
  • Two-level validation (syntactic + business)
  • Professional error handling
  • Appropriate HTTP codes
  • Standardized project structure

Proceed to TP3.1: Complete CRUD Architecture

Back to Module 3