Khởi đầu hành trình thiết kế kiến trúc backend hướng mô-đun

Trong thế giới phát triển phần mềm hiện đại, việc duy trì một mã nguồn lớn luôn là thách thức đối với mọi đội ngũ kỹ sư. Khi ứng dụng backend mở rộng quy mô, số lượng service, controller và provider tăng lên theo cấp số nhân. Nếu thiếu một cơ chế quản lý phụ thuộc chặt chẽ, hệ thống sẽ nhanh chóng rơi vào trạng thái spaghetti code khó bảo trì. NestJS xuất hiện như một lời giải hoàn hảo cho bài toán này nhờ việc áp dụng triệt để mô hình Dependency Injection từ những ngày đầu thiết kế.

Bản chất của Dependency Injection là đảo ngược quyền điều khiển trong lập trình. Thay vì một class tự khởi tạo các đối tượng phụ thuộc của nó thông qua từ khóa new, các đối tượng này sẽ được tiêm vào từ bên ngoài thông qua hàm khởi tạo. Điều này giúp giảm độ chặt giữa các module, tăng tính kiểm thử và tối ưu hóa khả năng tái sử dụng mã nguồn trong các dự án doanh nghiệp. Việc nắm vững cơ chế này đóng vai trò quyết định trong việc xây dựng các hệ thống phức tạp.

Để hiểu sâu về cách thức hoạt động bên dưới, chúng ta cần mổ xẻ cách hệ thống container quản lý vòng đời của các provider. Mỗi service khi được đánh dấu bằng decorator phù hợp sẽ được đăng ký vào bộ nhớ trung tâm và tự động giải quyết các mối quan hệ phụ thuộc mà lập trình viên không cần can thiệp thủ công. Đây cũng là nền tảng cốt lõi giúp các nhà phát triển dễ dàng mở rộng tính năng mà không làm ảnh hưởng đến cấu trúc tổng thể của ứng dụng.

Cơ chế hoạt động của IoC Container trong NestJS

IoC Container đóng vai trò là bộ não điều phối toàn bộ các thành phần trong ứng dụng. Khi khởi động, khung làm việc quét qua toàn bộ cấu trúc mã nguồn để xây dựng biểu đồ phụ thuộc dựa trên các decorator đã khai báo. Quá trình này diễn ra hoàn toàn tự động thông qua các siêu dữ liệu được cung cấp sẵn từ hệ thống TypeScript.

Mỗi khi một controller yêu cầu một service, IoC Container sẽ kiểm tra xem instance của service đó đã tồn tại trong bộ nhớ hay chưa. Nếu chưa, nó sẽ tiến hành khởi tạo mới cùng với tất cả các phụ thuộc cấp thấp hơn của service đó. Cơ chế này đảm bảo rằng các đối tượng được chia sẻ một cách hiệu quả trong toàn bộ vòng đời ứng dụng mà không gây ra tình trạng dư thừa tài nguyên.

Việc hiểu rõ cách thức IoC Container phân giải phụ thuộc giúp kỹ sư tránh được các lỗi kinh điển liên quan đến vòng lặp phụ thuộc giữa các module. Khi hai service gọi lẫn nhau trong hàm khởi tạo, hệ thống sẽ gặp bế tắc và không thể khởi động. Việc phát hiện sớm các vấn đề này từ giai đoạn thiết kế kiến trúc giúp tiết kiệm hàng giờ gỡ lỗi khi đưa ứng dụng lên môi trường vận hành thực tế.

Chiến lược phân rã Module hợp lý cho dự án lớn

Cấu trúc thư mục trong một dự án phần mềm phản ánh trực tiếp tư duy thiết kế của đội ngũ kỹ sư. Việc dồn tất cả logic vào một module gốc duy nhất là sai lầm phổ biến của những người mới tiếp cận công nghệ này. Thay vào đó, mỗi nghiệp vụ kinh doanh độc lập nên được đóng gói thành một module riêng biệt với ranh giới rõ ràng.

Một module chuẩn mực thường bao gồm controller chịu trách nhiệm tiếp nhận yêu cầu HTTP, service chứa logic xử lý nghiệp vụ và các repository tương tác trực tiếp với cơ sở dữ liệu. Việc phân chia này giúp các lập trình viên có thể làm việc song song trên các tính năng khác nhau mà ít khi xảy ra xung đột mã nguồn. Đồng thời, quá trình viết kiểm thử đơn vị cũng trở nên trơn tru hơn rất nhiều khi các thành phần được cô lập hoàn toàn.

Bên cạnh đó, việc tổ chức các module dùng chung như kết nối cơ sở dữ liệu hoặc hệ thống ghi log đòi hỏi một chiến lược cẩn trọng. Các module này cần được cấu hình dưới dạng Global Module để tránh việc phải lặp lại khai báo trong từng module tính năng riêng lẻ. Tuy nhiên, việc lạm dụng tính năng này có thể làm giảm tính minh bạch của biểu đồ phụ thuộc nên cần được cân nhắc kỹ lưỡng dựa trên quy mô thực tế của dự án.

Xử lý triệt để vòng lặp phụ thuộc giữa các Service

Vòng lặp phụ thuộc xảy ra khi Service A cần Service B để hoạt động, trong khi Service B lại đồng thời yêu cầu sự tồn tại của Service A trong hàm khởi tạo. Đây là bài toán nan giải trong thiết kế hệ thống phân tán nguyên khối lẫn vi dịch vụ. Khi ứng dụng phình to, việc vô tình tạo ra mối quan hệ chéo này rất dễ xảy ra nếu ranh giới trách nhiệm giữa các tầng nghiệp vụ không được vạch ra rạch ròi.

Trong các tình huống bất khả kháng khi không thể tách rời các module, hệ thống cung cấp sẵn cơ chế hỗ trợ thông qua forward reference. Cơ chế này cho phép hoãn thời điểm phân giải phụ thuộc cho đến khi các thành phần đã được khởi tạo đầy đủ trong bộ nhớ. Dù vậy, đây chỉ là giải pháp tình thế và việc lạm dụng nó cho thấy sự thiếu sót trong khâu thiết kế kiến trúc ban đầu.

Giải pháp bền vững hơn là tái cấu trúc lại nghiệp vụ bằng cách trích xuất phần logic chung sang một tầng thứ ba độc lập. Cả Service A và Service B đều có thể phụ thuộc vào tầng thứ ba này mà không tạo ra chu trình khép kín. Việc này đòi hỏi sự thấu hiểu sâu sắc về nghiệp vụ và khả năng bóc tách mã nguồn tinh tế từ người lập trình.

Kỹ thuật sử dụng ForwardRef giải quyết bế tắc

Khi gặp lỗi vòng lặp phụ thuộc, ứng dụng sẽ ngay lập tức từ chối khởi động và bắn ra thông báo lỗi chi tiết từ tầng khởi tạo. Để khắc phục nhanh chóng mà không phải đập đi xây lại toàn bộ cấu trúc, việc sử dụng hàm hỗ trợ từ hệ thống là phương án tối ưu. Kỹ thuật này thông báo cho trình quản lý phụ thuộc biết rằng hãy chờ đợi cho đến khi đối tượng đích sẵn sàng.

Dưới đây là một ví dụ thực chiến về cách áp dụng kỹ thuật này trong mã nguồn thực tế để kết nối hai service có mối quan hệ tương hỗ lẫn nhau:

import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { OrdersService } from './orders.service';

@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => OrdersService))
    private readonly ordersService: OrdersService,
  ) {}

  public async getUserActivity(userId: string): Promise<any> {
    const orders = await this.ordersService.findByUser(userId);
    return { userId, totalOrders: orders.length };
  }
}

Đoạn mã trên minh họa cách tiêm phụ thuộc vòng tròn một cách có kiểm soát thông qua hàm hỗ trợ. Bằng cách sử dụng biểu thức hàm trả về lớp đích, chúng ta vô hiệu hóa được thời điểm kiểm tra tức thời của bộ khởi tạo. Tuy nhiên, việc áp dụng cách này đòi hỏi sự cẩn trọng tuyệt đối để tránh việc gọi hàm đệ vô cực trong quá trình xử lý yêu cầu.

Tái cấu trúc thiết kế để loại bỏ quan hệ chéo

Giải pháp triệt để nhất cho vấn đề vòng lặp phụ thuộc là thay đổi tư duy thiết kế hệ thống theo hướng một chiều. Các tầng nghiệp vụ nên được phân cấp rõ ràng từ trên xuống dưới, trong đó tầng Controller gọi Service, và Service gọi Repository hoặc các dịch vụ hạ tầng thấp hơn. Tuyệt đối không để xảy ra hiện tượng service cấp thấp gọi ngược lại service cấp cao hơn.

Nếu hai tính năng cần chia sẻ chung một tập hợp dữ liệu hoặc hành vi, việc tách phần chung đó thành một module độc lập hoàn toàn là lựa chọn sáng suốt. Mô hình này không chỉ giúp mã nguồn sạch sẽ hơn mà còn tuân thủ tuyệt đối các nguyên lý thiết kế phần mềm hướng đối tượng hiện đại. Các kỹ sư có thể dễ dàng kiểm thử từng phần riêng biệt mà không lo ngại về các tác dụng phụ ngầm.

Quy trình tái cấu trúc này thường bắt đầu bằng việc vẽ lại biểu đồ tương tác giữa các thành phần trong hệ thống. Việc nhận diện rõ các điểm chạm dữ liệu giúp đội ngũ phát hiện ra những điểm nghẽn kiến trúc và đưa ra phương án chỉnh sửa phù hợp trước khi đưa tính năng ra môi trường sản xuất.

Tối ưu hóa vòng đời ứng dụng và quản lý tài nguyên

Hiểu về vòng đời ứng dụng là chìa khóa để kiểm soát tài nguyên hệ thống một cách hiệu quả. Mỗi khi ứng dụng khởi động hoặc dừng lại, hàng loạt sự kiện được kích hoạt để đảm bảo các kết nối mạng, kết nối cơ sở dữ liệu hay hàng đợi thông điệp được mở và đóng đúng thời điểm. Việc bỏ qua các bước dọn dẹp tài nguyên này có thể dẫn đến hiện tượng rò rỉ bộ nhớ hoặc mất mát dữ liệu khi hệ thống khởi động lại.

Các phương thức xử lý sự kiện vòng đời cho phép lập trình viên chèn thêm mã nguồn tùy chỉnh vào các thời điểm quan trọng như trước khi ứng dụng lắng nghe cổng mạng hoặc ngay trước khi tiến trình bị tắt hoàn toàn. Điều này cực kỳ quan trọng trong môi trường vận hành container, nơi các tiến trình thường xuyên được tạo mới hoặc hủy bỏ dựa trên tải thực tế của hệ thống.

Việc quản lý tốt vòng đời cũng giúp các dịch vụ kết nối ngoại vi có thời gian hoàn thành các tác vụ đang chạy dở dang trước khi ngắt kết nối hoàn toàn. Các kỹ sư vận hành luôn đánh giá cao những ứng dụng có khả năng tự dọn dẹp tài nguyên sạch sẽ vì nó giúp hệ thống duy trì tính ổn định cao trong mọi tình huống.

Khai thác các Lifecycle Hooks trong thực tế

Hệ thống cung cấp một tập hợp các giao diện định nghĩa sẵn để can thiệp vào từng giai đoạn sinh trưởng của một provider hoặc module. Các phương thức như khởi tạo, sẵn sàng hoạt động hay chuẩn bị hủy bỏ được gọi tự động theo một trình tự nghiêm ngặt. Việc tận dụng đúng các điểm móc này giúp tự động hóa hoàn toàn các thao tác thiết lập ban đầu.

Ví dụ điển hình là việc kiểm tra kết nối cơ sở dữ liệu ngay khi ứng dụng vừa khởi động xong để đảm bảo mọi thứ đã sẵn sàng tiếp nhận yêu cầu từ người dùng. Nếu kết nối thất bại, ứng dụng sẽ lập tức báo lỗi và dừng lại thay vì chấp nhận yêu cầu trong trạng thái mù mờ. Dưới đây là cách hiện thực hóa các phương thức này trong một service chuyên dụng:

import { Injectable, OnModuleInit, OnApplicationShutdown, Logger } from '@nestjs/common';
import { DataSource } from 'typeorm';

@Injectable()
export class DatabaseMonitorService implements OnModuleInit, OnApplicationShutdown {
  private readonly logger = new Logger(DatabaseMonitorService.name);

  constructor(private readonly dataSource: DataSource) {}

  public async onModuleInit(): Promise<void> {
    if (this.dataSource.isInitialized) {
      this.logger.log('Ket noi co so du lieu da san sang hoat dong.');
    }
  }

  public async onApplicationShutdown(signal?: string): Promise<void> {
    this.logger.warn(`Dang tat ung phieu do nhan duoc tin hieu: ${signal}`);
    await this.dataSource.destroy();
    this.logger.log('Da dong toan bo ket noi co so du lieu an toan.');
  }
}

Khối mã nguồn trên thể hiện rõ cách bắt các sự kiện quan trọng trong vòng đời của ứng dụng để thực hiện việc kiểm tra và dọn dẹp kết nối. Việc ghi lại log chi tiết trong từng phương thức giúp đội ngũ vận hành dễ dàng theo dõi trạng thái sức khỏe của hệ thống thông qua các công cụ giám sát tập trung.

Xử lý tín hiệu tắt ứng dụng an toàn trên môi trường Production

Trong môi trường vận hành thực tế, việc tắt một tiến trình không bao giờ nên diễn ra một cách đột ngột. Khi có lệnh dừng từ hệ thống điều phối container, ứng dụng cần một khoảng thời gian ngắn để hoàn tất các yêu cầu đang được xử lý dở dang và từ chối các kết nối mới đến. Quá trình này được gọi là tắt ứng dụng có kiểm soát.

Để kích hoạt tính năng này, việc bật cấu hình lắng nghe tín hiệu hệ thống là điều bắt buộc. Khi đó, các phương thức dọn dẹp tài nguyên sẽ có cơ hội được chạy trước khi hệ sinh thái giải phóng hoàn toàn vùng nhớ dành cho tiến trình. Điều này bảo vệ dữ liệu của người dùng không bị hỏng hóc do các thao tác ghi dữ liệu dang dở bị cắt ngang giữa chừng.

Việc kết hợp chặt chẽ giữa cơ chế quản lý vòng đời và các chiến lược kiểm tra sức khỏe định kỳ giúp hệ thống đạt độ sẵn sàng cao. Các kỹ sư có thể tự tin triển khai các phiên bản mới mà không lo làm gián đoạn trải nghiệm của người dùng cuối.

Xây dựng kiến trúc Custom Provider linh hoạt

Mặc dù cơ chế tiêm phụ thuộc mặc định đáp ứng tốt phần lớn nhu cầu thông thường, đôi khi các dự án doanh nghiệp đòi hỏi những cách thức khởi tạo đối tượng phức tạp hơn. Các nhà phát triển không chỉ đơn thuần muốn tiêm một lớp thông thường mà còn cần tiêm các giá trị tĩnh, các hàm cấu hình động hoặc các đối tượng được sinh ra dựa trên điều kiện thời gian chạy. Lúc này, khái niệm Custom Provider trở thành vũ khí bí mật của các lập trình viên chuyên nghiệp.

Custom Provider cho phép chúng ta toàn quyền kiểm soát cách mà một đối tượng được tạo ra và cung cấp cho toàn bộ hệ thống. Thay vì dựa vào định danh của lớp, chúng ta sử dụng các mã định danh tùy chỉnh kết hợp với các factory function để tính toán và trả về kết quả mong muốn. Khả năng tùy biến này mở ra không gian sáng tạo vô tận trong việc thiết kế các hệ thống linh hoạt.

Việc ứng dụng thành thạo các kỹ thuật này giúp mã nguồn trở nên uyển chuyển hơn rất nhiều khi phải tích hợp với các thư viện bên thứ ba vốn không được thiết kế sẵn để chạy trong mô hình hiện đại. Các đối tượng phức tạp sẽ được bao bọc gọn gàng đằng sau các giao diện chuẩn mực, giúp phần còn lại của ứng dụng không bị ảnh hưởng bởi những thay đổi từ môi trường bên ngoài.

Kỹ thuật Value Provider và Factory Provider chuyên sâu

Value Provider là cách đơn giản nhất để cung cấp các giá trị tĩnh như biến môi trường, chuỗi kết nối hoặc các cấu hình mặc định vào trong hệ thống mà không cần thông qua bước khởi tạo phức tạp. Trong khi đó, Factory Provider mạnh mẽ hơn rất nhiều khi cho phép thực thi một hàm logic để quyết định xem đối tượng nào sẽ được tạo ra dựa trên các tham số đầu vào tại thời điểm chạy.

Dưới đây là một ví dụ thực tế về cách sử dụng Factory Provider để khởi tạo một kết nối dịch vụ ngoài dựa trên cấu hình môi trường động:

import { Module, Provider } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ExternalApiClient } from './external-api.client';

const externalClientProvider: Provider = {
  provide: 'API_CLIENT',
  useFactory: async (configService: ConfigService) => {
    const apiKey = configService.get<string>('API_KEY');
    const timeout = configService.get<number>('API_TIMEOUT') || 5000;
    return new ExternalApiClient(apiKey, timeout);
  },
  inject: [ConfigService],
};

@Module({
  imports: [ConfigModule],
  providers: [externalClientProvider],
  exports: ['API_CLIENT'],
})
export class ExternalApiModule {}

Đoạn mã trên minh họa cách linh hoạt tạo ra một dịch vụ giao tiếp bên ngoài với các tham số được lấy trực tiếp từ cấu hình hệ thống một cách an toàn. Việc sử dụng định danh dạng chuỗi giúp che giấu hoàn toàn lớp cài đặt chi tiết bên dưới, cho phép thay đổi thư viện bên thứ ba bất cứ lúc nào mà không làm hỏng logic nghiệp vụ ở các module gọi đến nó.

Độ phức tạp của hệ thống sẽ giảm đi đáng kể khi các phụ thuộc bên ngoài được bọc gọn gàng trong các nhà cung cấp tùy chỉnh như vậy. Các lập trình viên có thể dễ dàng thay thế các đối tượng thực tế bằng các đối tượng giả lập trong quá trình viết kiểm thử tự động, từ đó nâng cao chất lượng tổng thể của toàn bộ dự án phần mềm.

Ứng dụng Alias Provider và Class Provider nâng cao

Bên cạnh Value và Factory, Alias Provider cho phép một định danh trỏ đến một instance của một provider khác đã tồn tại trong hệ thống. Kỹ thuật này cực kỳ hữu ích trong việc xây dựng các kiến trúc có tính trừu tượng cao, nơi các tầng nghiệp vụ chỉ làm việc với các giao diện trừu tượng thay vì các lớp cụ thể. Khi cần thay đổi cách cài đặt, chúng ta chỉ cần đổi ánh xạ trong module mà không cần sửa một dòng code nào ở tầng nghiệp vụ.

Class Provider lại mang lại sự linh hoạt trong việc quyết định lớp nào sẽ được hiện thực hóa dựa trên các điều kiện ngoại cảnh như hệ điều hành hoặc biến môi trường. Ví dụ, hệ thống có thể tự động sử dụng một phiên bản lưu trữ trên bộ nhớ đệm cục bộ khi chạy ở môi trường phát triển, nhưng tự động chuyển sang sử dụng phiên bản phân tán trên Redis khi chạy ở môi trường vận hành thực tế.

Sự kết hợp uyển chuyển giữa các loại nhà cung cấp này giúp tạo ra những hệ thống có khả năng thích ứng cực cao với các yêu cầu thay đổi liên tục từ phía doanh nghiệp. Đây cũng là ranh giới phân định rõ ràng giữa một lập trình viên thông thường và một kỹ sư kiến trúc phần mềm thực thụ.

Kiểm thử toàn diện ứng dụng với Dependency Injection

Kiểm thử tự động là tiêu chuẩn không thể thiếu trong bất kỳ dự án phần mềm chuyên nghiệp nào. Một kiến trúc tốt phải là một kiến trúc dễ dàng được kiểm thử từng phần một cách độc lập. Nhờ có cơ chế tiêm phụ thuộc xuyên suốt, việc thay thế các thành phần thực tế bằng các đối tượng giả lập trong quá trình viết kiểm thử trở nên vô cùng đơn giản và trực quan.

Thay vì khởi tạo toàn bộ cơ sở dữ liệu thật hoặc gọi tới các dịch vụ thanh toán trực tuyến bên ngoài, chúng ta có thể dễ dàng tạo ra các phiên bản giả lập để trả về kết quả mong muốn trong từng kịch bản kiểm thử cụ thể. Điều này giúp các bài kiểm thử chạy với tốc độ cực nhanh và không bao giờ bị ảnh hưởng bởi sự cố mạng hay trạng thái của các hệ thống ngoại vi.

Việc đầu tư thời gian vào việc viết kiểm thử đơn vị và kiểm thử tích hợp ngay từ đầu mang lại lợi ích lâu dài khổng lồ cho dự án. Đội ngũ kỹ sư có thể tự tin refactor mã nguồn mà không sợ làm gãy các tính năng cũ đã hoạt động ổn định trên môi trường sản xuất.

Kỹ thuật viết Unit Test hiệu quả cho Service phức tạp

Để viết một bài kiểm thử đơn vị chất lượng cao cho một service, chúng ta cần cô lập nó hoàn toàn khỏi các phụ thuộc bên ngoài bằng cách sử dụng các đối tượng thay thế. Khung kiểm thử cung cấp sẵn các công cụ mạnh mẽ để tạo ra các bản sao giả lập cho mọi provider mà service đó đang phụ thuộc vào.

Dưới đây là một ví dụ thực tế về cách thiết lập môi trường kiểm thử đơn vị cho một service xử lý nghiệp vụ người dùng:

import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';

describe('UsersService', () => {
  let service: UsersService;
  let mockRepository = {
    findOneBy: jest.fn().mockResolvedValue({ id: '1', name: 'Nguyen Van A' }),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
  });

  it('nen tim thay nguoi dung khi truyen dung dinh danh', async () => {
    const result = await service.getUserActivity('1');
    expect(result).toBeDefined();
    expect(mockRepository.findOneBy).toHaveBeenCalled();
  });
});

Đoạn mã trên thể hiện cách giả lập kho lưu trữ dữ liệu để kiểm tra logic bên trong của service mà không cần kết nối thực sự tới cơ sở dữ liệu. Cách tiếp cận này giúp các bài kiểm thử chạy độc lập hoàn toàn, có thể thực thi song song và đem lại kết quả chính xác tuyệt đối trong mọi thời điểm.

Việc duy trì độ bao phủ kiểm thử cao giúp đội ngũ phát hiện sớm các lỗi tiềm ẩn trước khi chúng có cơ hội lọt lên môi trường vận hành thực tế. Đây là nền tảng vững chắc để xây dựng những hệ thống phần mềm có độ tin cậy vượt trội.

Tối ưu hóa Integration Test cho toàn bộ Module

Bên cạnh kiểm thử đơn vị, kiểm thử tích hợp đóng vai trò kiểm tra sự phối hợp nhịp nhàng giữa nhiều module khác nhau trong hệ thống. Quá trình này đòi hỏi việc khởi tạo một phần hoặc toàn bộ ứng dụng trong môi trường kiểm thử để đảm bảo các controller, service và tầng lưu trữ dữ liệu hoạt động ăn khớp với nhau.

Việc tận dụng các cấu hình mô-đun linh hoạt giúp chúng ta dễ dàng thay thế cơ sở dữ liệu thật bằng một cơ sở dữ liệu trong bộ nhớ tạm thời mỗi khi chạy kiểm thử tích hợp. Điều này vừa đảm bảo tính chính xác của luồng xử lý thực tế vừa giữ cho thời gian thực thi của toàn bộ tập lệnh kiểm thử ở mức tối ưu nhất.

Quá trình xây dựng một hệ thống backend hiện đại đòi hỏi sự am hiểu sâu sắc về kiến trúc và các mô hình thiết kế tiên tiến. Nếu bạn muốn làm chủ hoàn toàn các kỹ thuật chuyên sâu này và tự tay xây dựng những ứng dụng backend quy mô lớn, hãy tham khảo khóa học RESTful API với NestJS & TypeORM tại Unicode Academy.