บทนำ: ทำไมต้องแบ่ง Layers?
เมื่อเขียนโปรแกรมขนาดใหญ่ หากไม่แบ่ง layers code จะ chaos:
text❌ ปัญหาของ Monolithic Code:
- Database code ปนกับ UI code
- Business logic ปนกับ data access
- ต้อง change UI ต้อง touch database code
- ต้อง change database ต้อง update UI
- ไม่รู้ flow ของ data
✓ Solutions:
- Organize code into layers
- Each layer ต่างกันชัดเจน
- Layers communicate through interfaces
- Change one layer ไม่ affect others
Layered Architecture = Organizing code into horizontal layers, each with specific responsibility
3-Layer Architecture: The Standard
Layer Structure
text┌─────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ (UI, Controllers, API endpoints) │
│ - Display data to user │
│ - Collect user input │
│ - Format responses │
└──────────────┬──────────────────────────┘
│ Calls
▼
┌─────────────────────────────────────────┐
│ DOMAIN/BUSINESS LOGIC LAYER │
│ (Services, Business Rules) │
│ - Process data │
│ - Apply business rules │
│ - Make decisions │
└──────────────┬──────────────────────────┘
│ Calls
▼
┌─────────────────────────────────────────┐
│ DATA/PERSISTENCE LAYER │
│ (Repositories, Database Access) │
│ - Read from database │
│ - Write to database │
│ - Data mapping │
└─────────────────────────────────────────┘
│ Connects
▼
[Database]
ตัวอย่างที่ 1: Monolithic vs Layered
❌ Monolithic (Bad)
java// ❌ ไม่ดี: Everything mixed together
@RestController
public class OrderController {
@PostMapping("/orders")
public String createOrder(@RequestBody OrderData data) {
// Step 1: Validate input (Presentation concern)
if (data.getCustomerId() == null) {
return "ERROR: Customer ID required";
}
// Step 2: Query database (Data concern)
Connection conn = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost/db");
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM customers WHERE id = ?"
);
stmt.setString(1, data.getCustomerId());
ResultSet rs = stmt.executeQuery();
if (!rs.next()) {
return "ERROR: Customer not found";
}
// Step 3: Business logic (Business concern)
double subtotal = 0;
for (OrderItem item : data.getItems()) {
// Query product price from database
PreparedStatement productStmt = conn.prepareStatement(
"SELECT price FROM products WHERE id = ?"
);
productStmt.setString(1, item.getProductId());
ResultSet productRs = productStmt.executeQuery();
if (productRs.next()) {
double price = productRs.getDouble("price");
subtotal += price * item.getQuantity();
}
}
// Apply discount logic
if (subtotal > 1000) {
subtotal *= 0.9; // 10% off
}
// Step 4: Update database
PreparedStatement updateStmt = conn.prepareStatement(
"INSERT INTO orders (customer_id, total, status) VALUES (?, ?, ?)"
);
updateStmt.setString(1, data.getCustomerId());
updateStmt.setDouble(2, subtotal);
updateStmt.setString(3, "PENDING");
updateStmt.executeUpdate();
// Step 5: Format response
return "SUCCESS: Order created with total: " + subtotal;
} catch (SQLException e) {
e.printStackTrace();
return "ERROR: Database error";
} finally {
if (conn != null) {
try { conn.close(); } catch (SQLException e) {}
}
}
}
}
// PROBLEMS:
// ❌ Controller ทำทุกอย่าง
// ❌ Database code mixed with business logic
// ❌ Can't test business logic without database
// ❌ Can't reuse business logic in other controllers
// ❌ Change database affect UI code
// ❌ 100+ lines in one method
✓ Layered (Good)
textProject structure:
├── controller/ (Presentation)
│ └── OrderController.java
├── service/ (Domain/Business)
│ ├── OrderService.java
│ └── DiscountService.java
├── repository/ (Data)
│ ├── OrderRepository.java
│ ├── CustomerRepository.java
│ └── ProductRepository.java
├── model/ (Shared)
│ ├── Order.java
│ ├── Customer.java
│ └── Product.java
└── config/
└── DatabaseConfig.java
Layer 1: Presentation Layer
Responsibility: Handle HTTP requests, format responses
java// Model for request/response
public class CreateOrderRequest {
private String customerId;
private List<OrderItemRequest> items;
// Getters/Setters
public String getCustomerId() { return customerId; }
public List<OrderItemRequest> getItems() { return items; }
}
public class OrderItemRequest {
private String productId;
private int quantity;
// Getters/Setters
public String getProductId() { return productId; }
public int getQuantity() { return quantity; }
}
public class OrderResponse {
private String status;
private String orderId;
private double total;
public OrderResponse(String status, String orderId, double total) {
this.status = status;
this.orderId = orderId;
this.total = total;
}
// Getters
public String getStatus() { return status; }
public String getOrderId() { return orderId; }
public double getTotal() { return total; }
}
// Controller
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// Inject service (dependency injection)
private OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
/**
* Create new order
* Responsibility: Parse input, call service, format response
*/
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@RequestBody CreateOrderRequest request) {
try {
// Validate input (presentation concern)
if (request.getCustomerId() == null || request.getCustomerId().isEmpty()) {
return ResponseEntity.badRequest()
.body(new OrderResponse("ERROR", "", 0));
}
// ← Call service (delegate business logic)
Order order = orderService.createOrder(
request.getCustomerId(),
request.getItems()
);
// ← Format response
OrderResponse response = new OrderResponse(
"SUCCESS",
order.getId(),
order.getTotal()
);
return ResponseEntity.ok(response);
} catch (CustomerNotFoundException e) {
return ResponseEntity.notFound().build();
} catch (Exception e) {
return ResponseEntity.status(500)
.body(new OrderResponse("ERROR", "", 0));
}
}
@GetMapping("/{orderId}")
public ResponseEntity<Order> getOrder(@PathVariable String orderId) {
try {
Order order = orderService.getOrder(orderId);
return ResponseEntity.ok(order);
} catch (OrderNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
}
// RESPONSIBILITY:
// ✓ Parse HTTP request
// ✓ Validate input format
// ✓ Call appropriate service
// ✓ Format HTTP response
// ✓ Handle HTTP-specific exceptions
Layer 2: Domain/Business Logic Layer
Responsibility: Process data, apply business rules
java// Business logic service
@Service
public class OrderService {
// Inject repositories (data access)
private OrderRepository orderRepository;
private CustomerRepository customerRepository;
private ProductRepository productRepository;
private DiscountService discountService;
public OrderService(
OrderRepository orderRepository,
CustomerRepository customerRepository,
ProductRepository productRepository,
DiscountService discountService) {
this.orderRepository = orderRepository;
this.customerRepository = customerRepository;
this.productRepository = productRepository;
this.discountService = discountService;
}
/**
* Create new order
* Responsibility: Business logic only
*/
public Order createOrder(String customerId, List<OrderItemRequest> items) {
// Step 1: Verify customer exists
Customer customer = customerRepository.findById(customerId);
if (customer == null) {
throw new CustomerNotFoundException("Customer not found: " + customerId);
}
// Step 2: Validate items
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
// Step 3: Calculate order total
double subtotal = calculateSubtotal(items);
// Step 4: Apply discount
double discount = discountService.calculateDiscount(customer, subtotal);
double total = subtotal - discount;
// Step 5: Create order object
Order order = new Order();
order.setCustomerId(customerId);
order.setItems(items);
order.setSubtotal(subtotal);
order.setDiscount(discount);
order.setTotal(total);
order.setStatus("PENDING");
order.setCreatedAt(new Date());
// Step 6: Persist order (delegate to repository)
return orderRepository.save(order);
}
/**
* Calculate subtotal for items
*/
private double calculateSubtotal(List<OrderItemRequest> items) {
double subtotal = 0;
for (OrderItemRequest item : items) {
// Get product info (repository handles data access)
Product product = productRepository.findById(item.getProductId());
if (product == null) {
throw new ProductNotFoundException(
"Product not found: " + item.getProductId()
);
}
// Apply business rule: can't order more than available stock
if (item.getQuantity() > product.getStock()) {
throw new InsufficientStockException(
"Not enough stock for: " + product.getName()
);
}
subtotal += product.getPrice() * item.getQuantity();
}
return subtotal;
}
public Order getOrder(String orderId) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new OrderNotFoundException("Order not found: " + orderId);
}
return order;
}
}
// Discount calculation service
@Service
public class DiscountService {
public double calculateDiscount(Customer customer, double subtotal) {
// Business rule: Premium customers get 10% off on orders > 1000
if (customer.isPremium() && subtotal > 1000) {
return subtotal * 0.1;
}
// Business rule: Orders > 500 get 5% off
if (subtotal > 500) {
return subtotal * 0.05;
}
return 0;
}
}
// RESPONSIBILITY:
// ✓ Apply business rules
// ✓ Validate business constraints
// ✓ Calculate/transform data
// ✓ Make business decisions
// ✓ Delegate data access to repositories
// ✗ NO database queries
// ✗ NO HTTP concerns
Layer 3: Data/Persistence Layer
Responsibility: Access and persist data
java// Repository interface
public interface OrderRepository {
Order findById(String orderId);
List<Order> findByCustomerId(String customerId);
Order save(Order order);
void update(Order order);
void delete(String orderId);
}
// Repository implementation
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Override
public Order findById(String orderId) {
String sql = "SELECT * FROM orders WHERE id = ?";
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, orderId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return mapResultSetToOrder(rs);
}
}
} catch (SQLException e) {
System.err.println("Error: " + e.getMessage());
}
return null;
}
@Override
public Order save(Order order) {
String sql = "INSERT INTO orders (id, customer_id, total, discount, status, created_at) " +
"VALUES (?, ?, ?, ?, ?, ?)";
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
String orderId = UUID.randomUUID().toString();
pstmt.setString(1, orderId);
pstmt.setString(2, order.getCustomerId());
pstmt.setDouble(3, order.getTotal());
pstmt.setDouble(4, order.getDiscount());
pstmt.setString(5, order.getStatus());
pstmt.setTimestamp(6, new java.sql.Timestamp(order.getCreatedAt().getTime()));
pstmt.executeUpdate();
order.setId(orderId);
return order;
} catch (SQLException e) {
System.err.println("Error: " + e.getMessage());
}
return null;
}
private Order mapResultSetToOrder(ResultSet rs) throws SQLException {
Order order = new Order();
order.setId(rs.getString("id"));
order.setCustomerId(rs.getString("customer_id"));
order.setTotal(rs.getDouble("total"));
order.setDiscount(rs.getDouble("discount"));
order.setStatus(rs.getString("status"));
order.setCreatedAt(new Date(rs.getTimestamp("created_at").getTime()));
return order;
}
}
// Repository interfaces for other entities
public interface CustomerRepository {
Customer findById(String customerId);
void save(Customer customer);
}
public interface ProductRepository {
Product findById(String productId);
List<Product> findAll();
}
// RESPONSIBILITY:
// ✓ Query database
// ✓ Map database rows to objects
// ✓ Persist objects to database
// ✗ NO business logic
// ✗ NO HTTP concerns
// ✗ NO formatting
ตัวอย่างที่ 2: How Layers Communicate
java// REQUEST FLOW (Controller → Service → Repository)
// 1. User sends HTTP request
POST /api/orders
Content-Type: application/json
{
"customerId": "CUST-001",
"items": [
{"productId": "PROD-001", "quantity": 2},
{"productId": "PROD-002", "quantity": 1}
]
}
// 2. Controller receives request
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(
@RequestBody CreateOrderRequest request) {
// ← Parse JSON to Java object (Presentation)
// ← Validate format
Order order = orderService.createOrder(
request.getCustomerId(),
request.getItems()
);
// ← Convert to response (Presentation)
return ResponseEntity.ok(new OrderResponse(...));
}
// 3. Service handles business logic
public Order createOrder(String customerId, List<OrderItemRequest> items) {
// Verify customer (calls repository)
Customer customer = customerRepository.findById(customerId);
// Calculate totals (business rules)
double subtotal = calculateSubtotal(items);
double discount = discountService.calculateDiscount(customer, subtotal);
// Create order
Order order = new Order(...);
// Persist (calls repository)
return orderRepository.save(order);
}
// 4. Repository persists data
public Order save(Order order) {
// Execute SQL
String sql = "INSERT INTO orders ...";
// ... execute ...
return order;
}
// 5. Response flows back
OrderService.createOrder() → returns Order object
OrderController.createOrder() → formats as OrderResponse
HTTP response:
{
"status": "SUCCESS",
"orderId": "ORD-12345",
"total": 950.00
}
ตัวอย่างที่ 3: Layered Testing
java// Each layer can be tested independently
// TEST 1: Domain logic (no database needed)
@Test
public void testDiscountCalculation() {
DiscountService discountService = new DiscountService();
// Mock customer (not real from database)
Customer premiumCustomer = new Customer();
premiumCustomer.setPremium(true);
// Test business rule
double discount = discountService.calculateDiscount(
premiumCustomer,
1500.0
);
assertEquals(150.0, discount); // 10% of 1500
}
// TEST 2: Service with mocked repository
@Test
public void testCreateOrderSuccess() {
// Mock repository
OrderRepository mockOrderRepo = mock(OrderRepository.class);
CustomerRepository mockCustomerRepo = mock(CustomerRepository.class);
ProductRepository mockProductRepo = mock(ProductRepository.class);
DiscountService discountService = new DiscountService();
// Setup mock data
Customer customer = new Customer();
customer.setId("CUST-001");
customer.setPremium(false);
when(mockCustomerRepo.findById("CUST-001"))
.thenReturn(customer);
Product product = new Product();
product.setId("PROD-001");
product.setPrice(100.0);
product.setStock(10);
when(mockProductRepo.findById("PROD-001"))
.thenReturn(product);
// Create service with mocks
OrderService orderService = new OrderService(
mockOrderRepo,
mockCustomerRepo,
mockProductRepo,
discountService
);
// Test without real database
List<OrderItemRequest> items = List.of(
new OrderItemRequest("PROD-001", 2)
);
Order order = orderService.createOrder("CUST-001", items);
assertEquals(200.0, order.getSubtotal());
assertEquals(0, order.getDiscount());
assertEquals(200.0, order.getTotal());
}
// TEST 3: Repository (with test database)
@Test
public void testOrderRepository() {
OrderRepository repository = new OrderRepositoryImpl();
Order order = new Order();
order.setCustomerId("CUST-001");
order.setTotal(1000.0);
order.setStatus("PENDING");
Order saved = repository.save(order);
assertNotNull(saved.getId());
Order retrieved = repository.findById(saved.getId());
assertEquals(1000.0, retrieved.getTotal());
}
// TEST 4: Controller (with mock service)
@Test
public void testOrderController() {
OrderService mockService = mock(OrderService.class);
OrderController controller = new OrderController(mockService);
Order mockOrder = new Order();
mockOrder.setId("ORD-001");
mockOrder.setTotal(950.0);
when(mockService.createOrder(any(), any()))
.thenReturn(mockOrder);
CreateOrderRequest request = new CreateOrderRequest();
request.setCustomerId("CUST-001");
ResponseEntity<OrderResponse> response =
controller.createOrder(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("SUCCESS", response.getBody().getStatus());
}
// BENEFITS:
// ✓ Test each layer independently
// ✓ Use mocks to isolate layers
// ✓ Fast tests (no database for service tests)
// ✓ Clear what's being tested
Best Practices: Layering Checklist
textPRESENTATION LAYER:
☑ Handle HTTP requests/responses only
☑ Validate input format
☑ Format output for API
☑ Don't contain business logic
☑ Don't query database directly
DOMAIN/BUSINESS LAYER:
☑ Contain business rules
☑ Validate business constraints
☑ Calculate/transform data
☑ Don't contain HTTP code
☑ Don't contain SQL code
DATA LAYER:
☑ Only data access operations
☑ Execute queries
☑ Map database to objects
☑ Don't contain business logic
☑ Don't contain HTTP code
COMMUNICATION:
☑ Layers call downward only
☑ Use interfaces/abstractions
☑ Dependency injection
☑ No circular dependencies
MISTAKES TO AVOID:
✗ Business logic in controller
✗ Database queries in controller
✗ Business logic in repository
✗ Layers calling upward
✗ Direct dependencies (tight coupling)
Visualization: Request Flow
text┌──────────────────────────────────────────────┐
│ CLIENT (Browser, Mobile App, etc) │
└──────────────┬───────────────────────────────┘
│ HTTP Request
▼
┌──────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ OrderController │
│ - Parse JSON │
│ - Validate format │
│ - Call OrderService │
└──────────────┬───────────────────────────────┘
│ createOrder(customerId, items)
▼
┌──────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ OrderService │
│ - Verify customer (→ repository) │
│ - Calculate total (business logic) │
│ - Apply discount (business logic) │
│ - Create order │
│ - Persist (→ repository) │
└──────────────┬───────────────────────────────┘
│ findById(), save()
▼
┌──────────────────────────────────────────────┐
│ DATA LAYER │
│ OrderRepository, CustomerRepository, etc │
│ - Query database │
│ - Map results to objects │
│ - Execute SQL │
└──────────────┬───────────────────────────────┘
│ SQL queries
▼
[DATABASE]
│
▲
(returns data)
│
└──────────────┬───────────────────────────────┘
│ DATA LAYER (returns objects) │
└──────────────┬───────────────────────────────┘
│ Order object
▼
┌──────────────────────────────────────────────┐
│ DOMAIN LAYER (returns Order object) │
└──────────────┬───────────────────────────────┘
│ Order object
▼
┌──────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ - Format Order as OrderResponse │
│ - Convert to JSON │
└──────────────┬───────────────────────────────┘
│ HTTP Response
▼
┌──────────────────────────────────────────────┐
│ CLIENT (Browser, Mobile App, etc) │
└──────────────────────────────────────────────┘
สรุป
Modularization & Layer (Presentation, Domain, Data) คือ foundation ของ scalable architecture:
3 Core Layers:
- Presentation → Handle UI/API concerns
- Domain → Apply business logic
- Data → Manage persistence
Key Principles:
- Separation of Concerns – Each layer ทำ one thing
- Layering – Layers communicate downward
- Abstraction – Use interfaces for loose coupling
- Testability – Each layer testable independently
ประโยชน์ของ Layering:
- Clear Structure – Everyone รู้ code ไปไหน
- Easy to Maintain – Change one layer ไม่affect others
- Easy to Test – Mock dependencies ได้
- Easy to Scale – Add features โดยไม่ผสมกับ existing
- Team Productivity – Multiple developers ทำงานพร้อมกัน
Common Layering Patterns:
- 3-Layer (Presentation, Business, Data) – Most common
- 4-Layer (add Service layer) – More separation
- Domain-Driven (complex business logic)
- CQRS (Command Query Responsibility Segregation)
Layering ไม่ใช่ “optional” แต่เป็น best practice ที่ professional developers follow – มันให้ structure, maintainability และ scalability ที่ necessary สำหรับ long-term project success ถ้าต้องเขียน code ที่อยู่ได้นาน layering คือ investment ที่ pay off
