Back to blog

Kiến Trúc Phân Lớp (N-Tier): Tách Biệt Mối Quan Tâm Trong Thực Tế

software-architecturebackenddesign-patternsspring-bootsystem-design
Kiến Trúc Phân Lớp (N-Tier): Tách Biệt Mối Quan Tâm Trong Thực Tế

Kiến trúc phân lớp có lẽ là pattern kiến trúc được sử dụng rộng rãi nhất trong phát triển phần mềm. Nếu bạn đã xây dựng ứng dụng Spring Boot với controllers, services và repositories — bạn đã dùng nó rồi. Nếu bạn đã xây dựng ứng dụng Express.js với routes, middleware và database calls — cũng vậy.

Nhưng sử dụng một pattern và hiểu sâu về nó là hai điều khác nhau. Kiến trúc phân lớp áp dụng kém dẫn đến leaky abstractions, anemic domain models, và code thì "có vẻ" phân lớp nhưng cấu trúc lại rất lộn xộn.

Trong bài viết này, chúng ta sẽ đề cập:

✅ Kiến trúc phân lớp là gì và tại sao nó tồn tại
✅ Các lớp tiêu chuẩn và trách nhiệm của chúng
✅ Phân lớp nghiêm ngặt vs linh hoạt — các đánh đổi
✅ Hướng phụ thuộc hoạt động như thế nào (và tại sao quan trọng)
✅ Các topology triển khai N-tier
✅ Triển khai thực tế trong Spring Boot và Express.js
✅ Lỗi phổ biến: anemic domain models, leaky abstractions
✅ Khi nào kiến trúc phân lớp không còn phù hợp


Kiến Trúc Phân Lớp Là Gì?

Kiến trúc phân lớp tổ chức code thành các lớp nằm ngang, trong đó mỗi lớp có một trách nhiệm cụ thể và chỉ giao tiếp với các lớp liền kề.

Ý tưởng cốt lõi: mỗi lớp chỉ giao tiếp với lớp ngay bên dưới nó. Tầng trình bày không biết dữ liệu được lưu trữ thế nào. Tầng truy cập dữ liệu không biết UI trông như thế nào.

Sự tách biệt mối quan tâm này là nền tảng của code sạch, có thể kiểm thử và dễ bảo trì.

Tại Sao Cần Kiến Trúc Phân Lớp?

Trước kiến trúc phân lớp, codebase thường là mớ hỗn độn của các câu SQL trộn lẫn với code render UI, logic nghiệp vụ rải rác trong các view template, và hoàn toàn không có sự tách biệt giữa các mối quan tâm.

Kiến trúc phân lớp mang lại kỷ luật:

  • Tách biệt mối quan tâm — mỗi lớp có một nhiệm vụ
  • Khả năng kiểm thử — mock lớp bên dưới và kiểm thử từng lớp độc lập
  • Tổ chức nhóm — frontend dev làm tầng trình bày, backend dev làm logic nghiệp vụ, DBA làm tầng truy cập dữ liệu
  • Khả năng thay thế — đổi database mà không cần sửa logic nghiệp vụ (về lý thuyết)

Các Lớp Tiêu Chuẩn

Lớp 1: Tầng Trình Bày

Còn gọi là: UI layer, web layer, interface layer, controller layer

Trách nhiệm: Chấp nhận các request đến, validate input, ủy quyền cho tầng nghiệp vụ, và định dạng response.

KHÔNG:

  • Chứa logic nghiệp vụ
  • Gọi database trực tiếp
  • Biết về cách dữ liệu được lưu trữ
// Spring Boot — Tầng Trình Bày
@RestController
@RequestMapping("/api/orders")
public class OrderController {
 
    private final OrderService orderService;
 
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
 
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
 
        // 1. Validate input (xử lý bởi @Valid)
        // 2. Ủy quyền cho service — KHÔNG có logic nghiệp vụ ở đây
        Order order = orderService.createOrder(request.toCommand());
 
        // 3. Định dạng response
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(OrderResponse.from(order));
    }
 
    @GetMapping("/{id}")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
        return orderService.findById(id)
                .map(order -> ResponseEntity.ok(OrderResponse.from(order)))
                .orElse(ResponseEntity.notFound().build());
    }
}
// Express.js — Tầng Trình Bày
const orderRouter = Router();
 
orderRouter.post('/', validateRequest(createOrderSchema), async (req, res) => {
  // Ủy quyền cho service — không có logic nghiệp vụ
  const order = await orderService.createOrder(req.body);
  res.status(201).json(OrderResponse.from(order));
});
 
orderRouter.get('/:id', async (req, res) => {
  const order = await orderService.findById(req.params.id);
  if (!order) return res.status(404).json({ error: 'Order not found' });
  res.json(OrderResponse.from(order));
});

Lớp 2: Tầng Logic Nghiệp Vụ

Còn gọi là: Service layer, application layer, domain layer

Trách nhiệm: Triển khai các quy tắc nghiệp vụ của ứng dụng. Điều phối các thao tác trên nhiều repositories. Thực thi các bất biến và ràng buộc.

KHÔNG:

  • Biết về HTTP, REST, hay cách nó được gọi
  • Truy cập database trực tiếp (sử dụng repository interfaces)
  • Biết về việc định dạng response
// Spring Boot — Tầng Logic Nghiệp Vụ
@Service
@Transactional
public class OrderService {
 
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final UserRepository userRepository;
    private final EventPublisher eventPublisher;
 
    public Order createOrder(CreateOrderCommand command) {
        // Quy tắc nghiệp vụ: user phải tồn tại
        User user = userRepository.findById(command.getUserId())
                .orElseThrow(() -> new UserNotFoundException(command.getUserId()));
 
        // Quy tắc nghiệp vụ: validate tất cả sản phẩm tồn tại và còn hàng
        List<OrderItem> items = command.getItems().stream()
                .map(item -> {
                    Product product = productRepository.findById(item.getProductId())
                            .orElseThrow(() -> new ProductNotFoundException(item.getProductId()));
 
                    if (product.getStock() < item.getQuantity()) {
                        throw new InsufficientStockException(product.getId());
                    }
 
                    return new OrderItem(product, item.getQuantity());
                })
                .collect(toList());
 
        // Tạo và lưu order
        Order order = Order.create(user, items);
        Order savedOrder = orderRepository.save(order);
 
        // Publish domain event (thông báo các phần khác của hệ thống)
        eventPublisher.publish(new OrderCreatedEvent(savedOrder));
 
        return savedOrder;
    }
}
// Express.js — Tầng Logic Nghiệp Vụ
export class OrderService {
  constructor(
    private readonly orderRepository: OrderRepository,
    private readonly productRepository: ProductRepository,
    private readonly userRepository: UserRepository,
  ) {}
 
  async createOrder(command: CreateOrderCommand): Promise<Order> {
    const user = await this.userRepository.findById(command.userId);
    if (!user) throw new UserNotFoundException(command.userId);
 
    const items = await Promise.all(
      command.items.map(async (item) => {
        const product = await this.productRepository.findById(item.productId);
        if (!product) throw new ProductNotFoundException(item.productId);
        if (product.stock < item.quantity) throw new InsufficientStockException(item.productId);
        return new OrderItem(product, item.quantity);
      }),
    );
 
    const order = Order.create(user, items);
    return this.orderRepository.save(order);
  }
}

Lớp 3: Tầng Truy Cập Dữ Liệu

Còn gọi là: Repository layer, persistence layer, DAO layer

Trách nhiệm: Trừu tượng hóa database. Chuyển đổi giữa domain objects và database rows/documents. Thực thi các câu query.

KHÔNG:

  • Chứa logic nghiệp vụ
  • Biết về HTTP hay tầng trình bày
  • Ra quyết định về dữ liệu cần fetch (đó là việc của service)
// Spring Boot — Tầng Truy Cập Dữ Liệu
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
 
    // Spring Data JPA tạo query từ tên method
    List<Order> findByUserIdOrderByCreatedAtDesc(Long userId);
 
    // Custom JPQL query
    @Query("SELECT o FROM Order o WHERE o.status = :status AND o.createdAt > :since")
    List<Order> findRecentByStatus(
            @Param("status") OrderStatus status,
            @Param("since") LocalDateTime since);
}
// Express.js — Tầng Truy Cập Dữ Liệu (dùng TypeORM)
@Injectable()
export class OrderRepository {
  constructor(
    @InjectRepository(OrderEntity)
    private readonly repo: Repository<OrderEntity>,
  ) {}
 
  async findByUserId(userId: string): Promise<Order[]> {
    const entities = await this.repo.find({
      where: { userId },
      order: { createdAt: 'DESC' },
      relations: ['items', 'items.product'],
    });
    return entities.map(Order.fromEntity);
  }
 
  async save(order: Order): Promise<Order> {
    const entity = order.toEntity();
    const saved = await this.repo.save(entity);
    return Order.fromEntity(saved);
  }
}

Lớp 4: Tầng Cơ Sở Dữ Liệu

Không hẳn là một lớp code — đây là database thực sự: PostgreSQL, MySQL, MongoDB, v.v.

Trong kiến trúc 3-tier, đây thường là một tier vật lý riêng biệt (một server khác). Trong môi trường phát triển, thường là cùng một máy.


Phân Lớp Nghiêm Ngặt vs Linh Hoạt

Một quyết định quan trọng khi áp dụng kiến trúc phân lớp: bạn thực thi quy tắc "chỉ nói chuyện với lớp liền kề" nghiêm ngặt đến mức nào?

Phân Lớp Nghiêm Ngặt

Mỗi lớp chỉ được gọi lớp ngay bên dưới nó.

Ưu điểm: Kiến trúc sạch, dễ hiểu về phụ thuộc, khả năng thay thế tối đa.

Nhược điểm: Đôi khi buộc bạn phải viết các method "pass-through" trong tầng service chỉ đơn giản là gọi repository mà không có logic nghiệp vụ thực sự — boilerplate không mang lại giá trị.

Phân Lớp Linh Hoạt

Các lớp có thể gọi bất kỳ lớp nào bên dưới chúng (không chỉ lớp liền kề).

Ưu điểm: Loại bỏ boilerplate pass-through cho các thao tác CRUD đơn giản.

Nhược điểm: Tầng trình bày bắt đầu bypass logic nghiệp vụ, dẫn đến validation bị trùng lặp và business rules rải rác khắp nơi.

Nên Dùng Loại Nào?

Mặc định dùng phân lớp nghiêm ngặt. Boilerplate biểu kiến của các method service pass-through là một sự đánh đổi đáng làm — nó giữ logic nghiệp vụ tập trung, có thể kiểm thử, và dễ phát triển.

Ngoại lệ duy nhất: thao tác CRUD đơn giản mà service method thực sự chỉ là pass-through với zero logic. Trong những trường hợp đó, phân lớp linh hoạt tiết kiệm thời gian mà không có nhược điểm đáng kể.


Hướng Phụ Thuộc

Hiểu hướng phụ thuộc là điều thiết yếu cho kiến trúc phân lớp có thể bảo trì. Quy tắc:

Lớp trên phụ thuộc vào lớp dưới. Lớp dưới không bao giờ phụ thuộc vào lớp trên.

Điều này có nghĩa là:

  • OrderController biết về OrderService — ổn.
  • OrderService biết về OrderRepository — ổn.
  • OrderRepository gọi OrderService — không bao giờ. Điều này tạo ra circular dependency.
  • OrderService biết về HTTP response codes — không bao giờ. Đó là kiến thức của tầng trình bày đang rò rỉ vào logic nghiệp vụ.

Dependency Inversion Để Tách Lớp

Công cụ mạnh nhất cho kiến trúc phân lớp sạch: phụ thuộc vào abstractions (interfaces), không phụ thuộc vào concrete implementations.

// ✅ Tầng nghiệp vụ phụ thuộc vào interface (abstraction)
@Service
public class OrderService {
    private final OrderRepository orderRepository;  // Interface, không phải concrete class
 
    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}
 
// Tầng truy cập dữ liệu cung cấp concrete implementation
@Repository
public class JpaOrderRepository implements OrderRepository {
    // ... JPA-specific implementation
}
 
// Để testing, inject mock implementation
@Mock
OrderRepository mockOrderRepository;

Bây giờ bạn có thể:

  • Test OrderService mà không cần database thực (inject mock)
  • Thay JpaOrderRepository bằng MongoOrderRepository mà không thay đổi OrderService
  • Thay Spring Data bằng raw JDBC mà không cần chạm vào logic nghiệp vụ

Các Topology Triển Khai N-Tier

"N-tier" đề cập đến việc triển khai vật lý các lớp trên các máy khác nhau — không chỉ là tổ chức code logic.

Kiến Trúc 2-Tier

Ứng dụng và database trên các máy riêng biệt. Hầu hết web app bắt đầu ở đây:

Dùng khi: Ứng dụng đơn giản, nhóm nhỏ, yêu cầu đơn giản.

Kiến Trúc 3-Tier

Các tier vật lý riêng biệt cho trình bày, ứng dụng và dữ liệu:

Dùng khi: Ứng dụng traffic cao cần scale web tier và application tier độc lập.

N-Tier với Caching và Messaging

Dùng khi: Hệ thống production với traffic cao, yêu cầu caching và xử lý background.


Triển Khai Thực Tế: Spring Boot

Ví dụ Spring Boot đầy đủ minh họa sự tách biệt lớp sạch:

src/main/java/com/example/store/
├── presentation/
│   ├── controller/
│   │   ├── OrderController.java
│   │   └── ProductController.java
│   └── dto/
│       ├── CreateOrderRequest.java
│       ├── OrderResponse.java
│       └── ProductResponse.java
├── business/
│   ├── service/
│   │   ├── OrderService.java
│   │   └── ProductService.java
│   └── domain/
│       ├── Order.java
│       ├── OrderItem.java
│       └── Product.java
├── data/
│   ├── repository/
│   │   ├── OrderRepository.java        # Interface
│   │   └── ProductRepository.java      # Interface
│   └── jpa/
│       ├── JpaOrderRepository.java     # Spring Data implementation
│       ├── JpaProductRepository.java
│       ├── OrderEntity.java
│       └── ProductEntity.java
└── StoreApplication.java

Các quyết định thiết kế chính trong cấu trúc này:

  1. DTOs vs Domain Objects — Tầng trình bày dùng DTOs (OrderResponse). Tầng nghiệp vụ dùng domain objects (Order). Tầng dữ liệu dùng entities (OrderEntity). Chúng là các class riêng biệt được map qua lại.

  2. Repository interfaces trong tầng nghiệp vụ — Interface OrderRepository nằm trong tầng nghiệp vụ, không phải tầng dữ liệu. Tầng dữ liệu cung cấp implementations. Đây là dependency inversion trong thực tế.

  3. Không có JPA annotations trong domain objects — Domain objects (Order.java) là các class Java thuần túy, không phải JPA entities. Điều này giữ logic nghiệp vụ thoát khỏi các concerns về persistence.


Triển Khai Thực Tế: Express.js / NestJS

src/
├── modules/
│   └── orders/
│       ├── presentation/
│       │   ├── orders.controller.ts
│       │   ├── dto/
│       │   │   ├── create-order.dto.ts
│       │   │   └── order-response.dto.ts
│       │   └── orders.module.ts
│       ├── business/
│       │   ├── orders.service.ts
│       │   ├── order.entity.ts          # Domain entity
│       │   └── order-repository.interface.ts  # Repository interface
│       └── data/
│           ├── typeorm-order.repository.ts   # TypeORM implementation
│           └── order.orm-entity.ts            # TypeORM entity
└── app.module.ts
// business/order-repository.interface.ts — interface trong tầng nghiệp vụ
export interface IOrderRepository {
  findById(id: string): Promise<Order | null>;
  findByUserId(userId: string): Promise<Order[]>;
  save(order: Order): Promise<Order>;
  delete(id: string): Promise<void>;
}
 
// data/typeorm-order.repository.ts — implementation trong tầng dữ liệu
@Injectable()
export class TypeOrmOrderRepository implements IOrderRepository {
  constructor(
    @InjectRepository(OrderOrmEntity)
    private readonly ormRepo: Repository<OrderOrmEntity>,
  ) {}
 
  async findById(id: string): Promise<Order | null> {
    const entity = await this.ormRepo.findOne({ where: { id }, relations: ['items'] });
    return entity ? Order.fromOrmEntity(entity) : null;
  }
 
  async save(order: Order): Promise<Order> {
    const entity = await this.ormRepo.save(order.toOrmEntity());
    return Order.fromOrmEntity(entity);
  }
}

Những Lỗi Phổ Biến

Lỗi 1: Anemic Domain Model

Lỗi phổ biến nhất và gây hại nhất trong kiến trúc phân lớp. Domain objects trở thành các túi dữ liệu rỗng — chỉ có getters và setters — với tất cả logic nhồi nhét vào services.

// ❌ Anti-pattern: Anemic domain model
public class Order {
    private Long id;
    private OrderStatus status;
    private List<OrderItem> items;
    private BigDecimal total;
 
    // Chỉ getters và setters. Không có hành vi.
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public OrderStatus getStatus() { return status; }
    public void setStatus(OrderStatus status) { this.status = status; }
    // ... 20 getters/setters nữa
}
 
// Tất cả logic trong service (thủ tục, không phải OOP)
@Service
public class OrderService {
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
 
        // Business rules rải rác trong service
        if (order.getStatus() == OrderStatus.DELIVERED) {
            throw new IllegalStateException("Không thể hủy order đã giao");
        }
        if (order.getStatus() == OrderStatus.CANCELLED) {
            throw new IllegalStateException("Order đã bị hủy rồi");
        }
 
        order.setStatus(OrderStatus.CANCELLED);  // Mutate state trực tiếp
        orderRepository.save(order);
    }
}
// ✅ Rich domain model — hành vi nằm cùng với dữ liệu
public class Order {
    private Long id;
    private OrderStatus status;
    private List<OrderItem> items;
 
    // Logic nghiệp vụ thuộc về domain object
    public void cancel() {
        if (this.status == OrderStatus.DELIVERED) {
            throw new IllegalStateException("Không thể hủy order đã giao");
        }
        if (this.status == OrderStatus.CANCELLED) {
            throw new IllegalStateException("Order đã bị hủy rồi");
        }
        this.status = OrderStatus.CANCELLED;
    }
 
    public BigDecimal calculateTotal() {
        return items.stream()
                .map(OrderItem::getSubtotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}
 
// Service giờ gọn gàng — điều phối, không trùng lặp logic
@Service
public class OrderService {
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.cancel();  // Logic domain được đóng gói
        orderRepository.save(order);
    }
}

Lỗi 2: Leaky Abstractions Giữa Các Lớp

Tầng trình bày biết về chi tiết database. Service layer trả về HTTP status codes. Repository layer format JSON. Tất cả đây đều là vi phạm lớp.

// ❌ Anti-pattern: Service layer rò rỉ HTTP concerns
@Service
public class OrderService {
    public ResponseEntity<OrderResponse> createOrder(CreateOrderRequest request) {
        // Service KHÔNG nên trả về ResponseEntity — đó là HTTP-specific
        // ...
        return ResponseEntity.status(201).body(new OrderResponse(order));
    }
}
 
// ❌ Anti-pattern: Presentation layer gọi repository trực tiếp
@RestController
public class OrderController {
    private final OrderRepository orderRepository;  // Bypass tầng nghiệp vụ
 
    @GetMapping("/{id}")
    public Order getOrder(@PathVariable Long id) {
        return orderRepository.findById(id).orElseThrow();  // Không áp dụng logic nghiệp vụ
    }
}
// ✅ Lớp sạch — mỗi lớp nói ngôn ngữ riêng của mình
@Service
public class OrderService {
    // Trả về domain objects, không phải HTTP responses
    public Order createOrder(CreateOrderCommand command) {
        // ...
        return savedOrder;  // Domain object, không phải HTTP response
    }
}
 
@RestController
public class OrderController {
    private final OrderService orderService;  // Chỉ nói chuyện với service
 
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
        Order order = orderService.createOrder(request.toCommand());
        return ResponseEntity.status(201).body(OrderResponse.from(order));
        // HTTP concerns được xử lý ở đây trong tầng trình bày
    }
}

Lỗi 3: Logic Nghiệp Vụ Trong Tầng Trình Bày

Validation và business rules bị rò rỉ vào controllers:

// ❌ Anti-pattern: Logic nghiệp vụ trong controller
router.post('/orders', async (req, res) => {
  const { userId, items } = req.body;
 
  // Logic nghiệp vụ này thuộc về service
  for (const item of items) {
    const product = await productRepo.findById(item.productId);
    if (product.stock < item.quantity) {
      return res.status(400).json({ error: `Không đủ hàng cho ${product.name}` });
    }
  }
 
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  if (total < 10) {
    return res.status(400).json({ error: 'Giá trị đơn hàng tối thiểu là $10' });
  }
 
  // ...
});
 
// ✅ Controller ủy quyền, service xử lý logic
router.post('/orders', async (req, res) => {
  try {
    const order = await orderService.createOrder(req.body);
    res.status(201).json(OrderResponse.from(order));
  } catch (error) {
    if (error instanceof InsufficientStockError) {
      return res.status(400).json({ error: error.message });
    }
    if (error instanceof MinimumOrderValueError) {
      return res.status(400).json({ error: error.message });
    }
    throw error;
  }
});

Lỗi 4: Repository "Thông Minh"

Repositories ra quyết định mà chúng không nên — lọc dựa trên business rules, join tables dựa trên business context:

// ❌ Anti-pattern: Logic nghiệp vụ trong repository
@Repository
public class OrderRepository {
    // Repository quyết định "gần đây" nghĩa là gì và cái gì là "quan trọng"
    // Đây là logic nghiệp vụ, không phải logic truy cập dữ liệu
    public List<Order> findImportantRecentOrders() {
        return entityManager.createQuery(
            "SELECT o FROM Order o " +
            "WHERE o.total > 500 " +           // Business rule: "quan trọng" là gì?
            "AND o.createdAt > :lastWeek " +   // Business rule: "gần đây" là gì?
            "AND o.status != 'CANCELLED'"      // Business rule: loại trừ đã hủy
        ).setParameter("lastWeek", LocalDateTime.now().minusWeeks(1))
         .getResultList();
    }
}
 
// ✅ Repository xử lý cơ chế, service ra quyết định
@Repository
public class OrderRepository {
    // Query chung — chỉ là data access concern
    public List<Order> findByStatusAndCreatedAfter(OrderStatus status, LocalDateTime since) {
        return entityManager.createQuery(
            "SELECT o FROM Order o WHERE o.status = :status AND o.createdAt > :since"
        ).setParameter("status", status)
         .setParameter("since", since)
         .getResultList();
    }
}
 
@Service
public class OrderService {
    // Logic nghiệp vụ nằm trong service
    public List<Order> findImportantRecentOrders() {
        LocalDateTime lastWeek = LocalDateTime.now().minusWeeks(1);
        List<Order> recentOrders = orderRepository.findByStatusAndCreatedAfter(
            OrderStatus.ACTIVE, lastWeek);
 
        // Business rule được áp dụng trong service
        return recentOrders.stream()
                .filter(order -> order.getTotal().compareTo(new BigDecimal("500")) > 0)
                .collect(toList());
    }
}

Khi Nào Kiến Trúc Phân Lớp Không Còn Phù Hợp

Kiến trúc phân lớp là lựa chọn mặc định vững chắc, nhưng nó có những điểm yếu.

Vấn Đề 1: Feature Silos vs Layer Silos

Khi ứng dụng phát triển, các nhóm được tổ chức theo lớp (nhóm tất cả controllers, nhóm tất cả services) gặp khó khăn trong việc phối hợp cross-layer. Một thay đổi feature đơn giản đụng đến controller, service, và repository — đòi hỏi phối hợp giữa nhiều nhóm.

Giải pháp: Tổ chức theo feature modules, mỗi module có mini layered stack riêng. Đây là pattern modular monolith từ ARCH-2.

modules/
├── orders/
│   ├── OrderController.java
│   ├── OrderService.java
│   └── OrderRepository.java
├── products/
│   ├── ProductController.java
│   ├── ProductService.java
│   └── ProductRepository.java

Mỗi feature sở hữu vertical slice của mình. Các nhóm có thể làm việc trên features một cách độc lập.

Vấn Đề 2: Performance Overhead

Phân lớp nghiêm ngặt có nghĩa là mỗi lần truy cập dữ liệu đi qua nhiều objects: controller → service → repository → database → ngược lại. Với các đường xử lý high-performance có hàng nghìn requests mỗi giây, overhead này quan trọng.

Giải pháp: Profile trước. Nếu một đường cụ thể là bottleneck, hãy xem xét:

  • Caching ở tầng service
  • Read models / query objects bypass domain model (CQRS, đề cập trong ARCH-7)
  • Database views cho các read queries phức tạp

Vấn Đề 3: Domain Logic Phức Tạp Trở Nên Khó Quản Lý

Với các domain nghiệp vụ phức tạp (tài chính, y tế, logistics), một tầng service phẳng với các services nặng về orchestration trở thành ác mộng điều phối. Services cần gọi các services khác, tạo ra dependency chains khó kiểm thử và lý giải.

Giải pháp: Xem xét các patterns kiến trúc phong phú hơn:

  • Hexagonal Architecture (ARCH-8) — để testability tốt hơn và cô lập hệ thống bên ngoài
  • Domain-Driven Design (ARCH-10) — cho domain nghiệp vụ phức tạp với hành vi phong phú
  • CQRS (ARCH-7) — khi read và write models phân kỳ đáng kể

Vấn Đề 4: Distributed Layering Trong Microservices

Trong thế giới microservices, "layers" trở thành toàn bộ services. "Gọi tầng service" có nghĩa là một HTTP request. Điều này dẫn đến network latency, partial failure scenarios, và distributed tracing requirements không tồn tại trong một layered monolith đơn giản.

Giải pháp: Cân nhắc kỹ lưỡng khi nào bạn chuyển từ layered monolith sang distributed services. Chiến lược monolith-first áp dụng ở đây.


Kiến Trúc Phân Lớp vs Các Patterns Khác

Khía CạnhKiến Trúc Phân LớpHexagonal ArchitectureClean Architecture
Tổ chứcLớp nằm ngang (tech concern)Ports & adapters (inside-out)Vòng đồng tâm (policy vs detail)
Quy tắc phụ thuộcTop-downLuôn hướng vào trongLuôn hướng vào trong
Cô lập domainMột phầnMạnhRất mạnh
Khả năng kiểm thửTốtXuất sắcXuất sắc
Đường cong học tậpThấpTrung bìnhCao
Phù hợp nhấtHầu hết CRUD appsDomain phức tạp, nhiều hệ thống bên ngoàiDomain phức tạp, hệ thống tồn tại lâu

Với hầu hết web applications và REST APIs, kiến trúc phân lớp là điểm khởi đầu đúng đắn. Chuyển sang hexagonal hay clean architecture khi bạn cảm nhận được những điểm đau cụ thể mà chúng giải quyết.


Kiểm Thử Kiến Trúc Phân Lớp

Pattern phân lớp cho phép cô lập kiểm thử xuất sắc khi được thực hiện đúng cách:

// Unit test cho tầng service — mock repository
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
 
    @Mock
    private OrderRepository orderRepository;
 
    @Mock
    private ProductRepository productRepository;
 
    @InjectMocks
    private OrderService orderService;
 
    @Test
    void shouldThrowWhenProductOutOfStock() {
        // Arrange
        Product product = new Product("prod-1", "Widget", 5); // stock: 5
        given(productRepository.findById("prod-1")).willReturn(Optional.of(product));
 
        CreateOrderCommand command = new CreateOrderCommand(
            "user-1",
            List.of(new OrderItemCommand("prod-1", 10)) // Yêu cầu 10, chỉ có 5 trong kho
        );
 
        // Act + Assert
        assertThatThrownBy(() -> orderService.createOrder(command))
                .isInstanceOf(InsufficientStockException.class);
 
        verify(orderRepository, never()).save(any()); // Order không nên được lưu
    }
}
 
// Integration test cho tầng repository — test với database thực
@DataJpaTest
class OrderRepositoryTest {
 
    @Autowired
    private JpaOrderRepository orderRepository;
 
    @Test
    void shouldFindOrdersByUserId() {
        // ... test với H2 in-memory database
    }
}
 
// Integration test cho tầng trình bày — mock service
@WebMvcTest(OrderController.class)
class OrderControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockBean
    private OrderService orderService;
 
    @Test
    void shouldReturn201WhenOrderCreated() throws Exception {
        given(orderService.createOrder(any())).willReturn(testOrder());
 
        mockMvc.perform(post("/api/orders")
                .contentType(APPLICATION_JSON)
                .content("""
                    {"userId": "1", "items": [{"productId": "p1", "quantity": 2}]}
                    """))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").exists());
    }
}

Mỗi lớp được kiểm thử độc lập với công cụ phù hợp:

  • Tầng service: Unit tests thuần túy với mocks — nhanh, không có I/O
  • Tầng repository: Slice tests với embedded database — integration nhưng tập trung
  • Tầng trình bày: MockMvc / supertest — cấp độ HTTP không cần DB thực

Tóm Tắt

Kiến trúc phân lớp cung cấp nền tảng đã được chứng minh để xây dựng web applications có thể bảo trì:

Nguyên tắc cốt lõi:

  • Mỗi lớp có một trách nhiệm rõ ràng, duy nhất
  • Phụ thuộc chỉ chảy xuống dưới
  • Lớp trên phụ thuộc vào abstractions, không phụ thuộc vào concrete implementations
  • Logic nghiệp vụ thuộc về tầng nghiệp vụ — không phải controllers, không phải repositories

Những lỗi phổ biến cần tránh:

  • Anemic domain models (logic trong service, domain objects rỗng)
  • Vi phạm lớp (controllers gọi repositories, services trả về HTTP responses)
  • Logic nghiệp vụ trong tầng trình bày
  • Smart repositories mã hóa các quyết định nghiệp vụ

Khi nào dùng kiến trúc phân lớp:

  • Hầu hết CRUD applications và REST APIs
  • Các nhóm mới bắt đầu với architecture patterns
  • Hệ thống mà domain complexity còn quản lý được
  • Điểm khởi đầu trước khi xem xét các patterns phức tạp hơn

Khi nào nên phát triển:

  • Domain logic trở nên quá phức tạp cho một tầng service
  • Cần testability mạnh hơn và cô lập hệ thống bên ngoài → Hexagonal Architecture
  • Read và write models phân kỳ đáng kể → CQRS
  • Ownership nhóm phân tán trở nên quan trọng → Microservices

Mục tiêu không phải là tuân theo pattern một cách hoàn hảo — mà là cung cấp phần mềm dễ hiểu, kiểm thử và thay đổi.


Tiếp Theo Trong Series Software Architecture

Đây là bài viết số 3 trong 12 của series Software Architecture Patterns:

Bài viết liên quan:

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.