jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Node.js Super Senior · Phase 10 — Enterprise Architecture

The capstone: clean/layered architecture, microservices with an API gateway, event-driven design, CQRS, core design patterns (Singleton, Factory, Observer, Decorator), and SOLID — what separates a senior from a super senior.

This is Phase 10 — the capstone of the 10-phase Super Senior path {Đây là Phase 10 — bài tổng kết của lộ trình Super Senior 10 phase}. You can build secure, fast, well-tested, deployable APIs {Bạn dựng được API an toàn, nhanh, test kỹ, deploy được}. The final leap is architecture: organizing systems so they stay changeable, scalable, and understandable as they grow for years across many engineers {Bước nhảy cuối là kiến trúc: tổ chức hệ thống để nó vẫn dễ thay đổi, mở rộng, và hiểu được khi lớn lên nhiều năm qua nhiều kỹ sư}. This is the thinking that separates a senior from a super senior {Đây là tư duy phân biệt senior với super senior}.


10.1 Clean (layered) architecture {Kiến trúc clean (phân tầng)}

The core idea: dependencies point inward {Ý tưởng cốt lõi: phụ thuộc hướng vào trong}. The domain (business rules) knows nothing about Express, the database, or HTTP — those are outer details that depend on the core, never the reverse {Domain (luật nghiệp vụ) không biết gì về Express, database, hay HTTP — đó là chi tiết bên ngoài phụ thuộc vào lõi, không bao giờ ngược lại}.

src/
├── presentation/    # controllers, routes, middleware (HTTP details)
├── application/     # use cases / services, DTOs, validators
├── domain/          # entities, interfaces, business rules (no framework!)
├── infrastructure/  # repositories, ORM, external APIs (implements domain interfaces)
└── shared/          # config, constants, utils
// domain/User.ts — pure business logic, zero dependencies
export class User {
  constructor(
    public readonly id: string | null,
    public readonly email: string,
    private readonly passwordHash: string,
    public readonly createdAt: Date,
  ) {}

  verifyPassword(plain: string): Promise<boolean> {
    return bcrypt.compare(plain, this.passwordHash);
  }
}

// application/RegisterUserService.ts — a use case orchestrating the domain
export class RegisterUserService {
  constructor(
    private readonly users: UserRepository,   // an interface, not a concrete class
    private readonly email: EmailService,
  ) {}

  async execute(email: string, password: string) {
    if (await this.users.findByEmail(email)) throw new AppError('Email already registered', 409);
    const user = new User(null, email, await bcrypt.hash(password, 10), new Date());
    await this.users.save(user);
    await this.email.sendWelcome(email);
    return { id: user.id, email: user.email };
  }
}

// presentation/AuthController.ts — only HTTP concerns
export class AuthController {
  constructor(private readonly registerUser: RegisterUserService) {}
  register = async (req: Request, res: Response, next: NextFunction) => {
    try {
      res.status(201).json(await this.registerUser.execute(req.body.email, req.body.password));
    } catch (err) { next(err); }
  };
}

The payoff {Phần thưởng}: you can swap Postgres for Mongo (rewrite infrastructure only), or Express for Fastify (rewrite presentation only), without touching business rules {bạn có thể đổi Postgres sang Mongo (chỉ viết lại infrastructure), hay Express sang Fastify (chỉ viết lại presentation), mà không chạm luật nghiệp vụ}. This is the repository + service pattern from Phase 6, taken to its logical conclusion {Đây là mẫu repository + service từ Phase 6, đẩy tới kết luận hợp lý}.

This same shape has many names — Hexagonal / Ports & Adapters / Onion architecture {Cùng hình dạng này có nhiều tên — kiến trúc Hexagonal / Ports & Adapters / Onion}. The domain defines ports (interfaces like UserRepository); infrastructure provides adapters (a Postgres implementation). The core depends only on ports, so adapters are pluggable {Domain định nghĩa port (interface); infrastructure cung cấp adapter (cài đặt cụ thể). Lõi chỉ phụ thuộc port, nên adapter cắm được}.

Domain-Driven Design vocabulary {Từ vựng Domain-Driven Design}

When the domain is complex, DDD gives the shared language to model it {Khi domain phức tạp, DDD cho ngôn ngữ chung để mô hình hóa}:

  • Entity — has identity over time (a User with an id) {có danh tính theo thời gian}.
  • Value object — defined by its values, immutable (Money, Email) {định nghĩa bởi giá trị, bất biến}.
  • Aggregate — a cluster of objects with one root that guards invariants; you load/save the whole aggregate, and reference other aggregates by id only {cụm đối tượng có một root giữ bất biến; tham chiếu aggregate khác bằng id}.
  • Bounded context — an explicit boundary where a term has one meaning; each becomes a service or module {ranh giới rõ nơi một thuật ngữ có một nghĩa}.
  • Ubiquitous language — code uses the same words as the domain experts {code dùng đúng từ của chuyên gia nghiệp vụ}.

The aggregate boundary is also your transaction boundary and a natural seam for later splitting into services {Ranh giới aggregate cũng là ranh giới transaction và đường nối tự nhiên để sau này tách thành service}.


10.2 Microservices architecture {Kiến trúc microservices}

A monolith becomes hard to scale and deploy past a certain size {Một monolith trở nên khó scale và deploy quá một kích thước nhất định}. Microservices split it into independently deployable services, each owning its data {Microservices chia nó thành các service deploy độc lập, mỗi cái sở hữu dữ liệu riêng}.

                ┌─────────────┐
   client ─────▶│ API Gateway │ (auth, rate-limit, routing — cross-cutting)
                └──────┬──────┘
        ┌─────────────┼─────────────┐
        ▼             ▼             ▼
  user-service   post-service   notif-service
        │             │             │
     user-db       post-db       notif-db   (a database per service)
// API Gateway — one entry point; cross-cutting concerns live here
import httpProxy from 'express-http-proxy';

app.use(authenticate);   // do auth/rate-limit/logging ONCE at the edge
app.use(rateLimit);
app.use('/api/users', httpProxy('http://user-service:3001'));
app.use('/api/posts', httpProxy('http://post-service:3002'));

The honest senior take {Quan điểm senior thành thật}: microservices trade code complexity for operational and distributed-systems complexity (network failures, eventual consistency, distributed tracing) {microservices đánh đổi độ phức tạp code lấy độ phức tạp vận hành và hệ phân tán (lỗi mạng, nhất quán cuối, distributed tracing)}. Start with a well-layered monolith (10.1) and extract services only when team size or scaling pressure demands it {Bắt đầu bằng monolith phân tầng tốt (10.1) và tách service chỉ khi quy mô team hoặc áp lực scale đòi hỏi}.

The distributed-systems patterns you must name {Các mẫu hệ phân tán phải gọi tên được}

  • Saga — there are no cross-service ACID transactions, so model a multi-service workflow as a sequence of local transactions with compensating actions on failure (refund if shipping fails). Orchestration = a coordinator drives the steps; choreography = services react to each other’s events {không có transaction ACID xuyên service, nên mô hình hóa workflow bằng chuỗi transaction cục bộ với hành động bù khi lỗi}.
  • Outbox — to update the DB and publish an event atomically, write the event to an outbox table in the same transaction, then a relay publishes it (Phase 6) {ghi sự kiện vào bảng outbox trong cùng transaction rồi relay phát}.
  • Idempotency — networks retry, so every consumer must handle the same message twice safely (dedupe by message id) {mạng retry, nên mọi consumer phải xử lý cùng message hai lần an toàn}.
  • Resilience at the boundary — timeouts, retries with backoff, and circuit breakers on every cross-service call (Phase 6) {timeout, retry có backoff, và circuit breaker ở mọi lời gọi xuyên service}.
  • Distributed tracing — propagate a trace id across services (OpenTelemetry) so one request is followable end-to-end {lan truyền trace id để theo dõi một request đầu-cuối}.

10.3 Event-driven architecture {Kiến trúc hướng sự kiện}

Instead of services calling each other directly (tight coupling), they publish events and others react {Thay vì service gọi nhau trực tiếp (gắn chặt), chúng phát sự kiện và bên khác phản ứng}. The publisher doesn’t know or care who listens {Bên phát không biết và không quan tâm ai nghe}.

import { EventEmitter } from 'node:events';

class DomainEventBus extends EventEmitter {
  publish(event: { type: string }) { this.emit(event.type, event); }
  subscribe(type: string, handler: (e: unknown) => void) { this.on(type, handler); }
}

// The register use case just announces what happened...
await this.users.save(user);
this.bus.publish({ type: 'UserRegistered', userId: user.id, email: user.email });

// ...and independent handlers react, each in its own module:
bus.subscribe('UserRegistered', (e) => emailService.sendWelcome(e.email));
bus.subscribe('UserRegistered', (e) => analytics.track('signup', e.userId));

Adding “send a Slack alert on signup” means adding one new subscriber — you never touch the registration code {Thêm “gửi cảnh báo Slack khi đăng ký” nghĩa là thêm một subscriber mới — không bao giờ chạm code đăng ký}. For cross-service/durable events, replace the in-process EventEmitter with a broker — RabbitMQ (amqplib), Redis Streams, or Kafka {Cho sự kiện xuyên service/bền, thay EventEmitter trong tiến trình bằng broker — RabbitMQ, Redis Streams, hoặc Kafka}.


10.4 CQRS {CQRS}

Command Query Responsibility Segregation: separate the write model (commands that change state) from the read model (queries that return data) {Tách mô hình ghi (command đổi trạng thái) khỏi mô hình đọc (query trả dữ liệu)}. They have different needs — writes need validation and consistency; reads need speed and denormalization {Chúng có nhu cầu khác nhau — ghi cần validation và nhất quán; đọc cần tốc độ và denormalization}.

class RegisterUserCommand { constructor(public email: string, public password: string) {} }

class RegisterUserHandler {
  constructor(private readonly users: UserRepository) {}
  handle(cmd: RegisterUserCommand) { return this.users.create({ email: cmd.email, password: cmd.password }); }
}

class CommandBus {
  private handlers = new Map<string, { handle: (c: unknown) => unknown }>();
  register(type: string, handler: { handle: (c: unknown) => unknown }) { this.handlers.set(type, handler); }
  execute(cmd: object) { return this.handlers.get(cmd.constructor.name)!.handle(cmd); }
}

CQRS pairs naturally with event-driven systems and event sourcing, but it adds complexity — use it for complex domains with asymmetric read/write loads, not simple CRUD {CQRS hợp tự nhiên với hệ hướng sự kiện và event sourcing, nhưng thêm phức tạp — dùng cho domain phức tạp có tải đọc/ghi bất đối xứng, không phải CRUD đơn giản}.

Event sourcing takes it further: instead of storing current state, store the append-only log of events and rebuild state by replaying them {Event sourcing đi xa hơn: thay vì lưu trạng thái hiện tại, lưu log sự kiện chỉ-thêm và dựng lại trạng thái bằng cách phát lại}. You gain a perfect audit trail and time-travel, at the cost of versioning events and building read projections {Bạn được audit trail hoàn hảo và du hành thời gian, đổi lại phải version sự kiện và dựng read projection}. Powerful, but reach for it only when the domain truly needs history {Mạnh, nhưng chỉ dùng khi domain thật sự cần lịch sử}.


10.5 Core design patterns {Design pattern cốt lõi}

// Singleton — one shared instance (DB connection, config)
class Database {
  private static instance: Database;
  static getInstance(): Database { return (this.instance ??= new Database()); }
}

// Factory — create objects without hard-coding the concrete class
class PaymentGatewayFactory {
  static create(type: 'stripe' | 'paypal'): PaymentGateway {
    if (type === 'stripe') return new StripeGateway();
    if (type === 'paypal') return new PayPalGateway();
    throw new Error(`Unknown gateway: ${type}`);
  }
}

// Observer — the EventEmitter / pub-sub pattern (10.3)
// Decorator — wrap behavior (e.g. logging) without changing the original
function withLogging<T extends (...a: never[]) => Promise<unknown>>(fn: T, name: string): T {
  return (async (...args) => {
    logger.info(`→ ${name}`);
    const result = await fn(...args);
    logger.info(`← ${name}`);
    return result;
  }) as T;
}

A senior reaches for patterns to name and reuse a proven solution — never to look clever {Senior dùng pattern để đặt tên và tái dùng một giải pháp đã được kiểm chứng — không bao giờ để tỏ ra thông minh}. The wrong pattern is worse than none {Pattern sai còn tệ hơn không có}.


10.6 SOLID principles {Nguyên tắc SOLID}

The five principles underpinning everything above {Năm nguyên tắc nền cho mọi thứ ở trên}:

  • Single Responsibility — one reason to change. User holds data; UserRepository persists; EmailService emails — not one god-class doing all three {Một lý do để đổi. User giữ dữ liệu; UserRepository lưu; EmailService gửi email — không phải một god-class làm cả ba}.
  • Open/Closed — open for extension, closed for modification. Add a new PaymentGateway without editing PaymentProcessor {Mở để mở rộng, đóng để sửa. Thêm PaymentGateway mới mà không sửa PaymentProcessor}.
  • Liskov Substitution — any PaymentGateway subtype must work wherever the base is expected {Mọi subtype PaymentGateway phải chạy được ở nơi mong đợi base}.
  • Interface Segregation — many small, focused interfaces beat one fat interface {Nhiều interface nhỏ, tập trung tốt hơn một interface to}.
  • Dependency Inversion — depend on abstractions, not concretions {Phụ thuộc trừu tượng, không phải cụ thể}:
// ✅ OrderService depends on the abstraction — that's why DI (Phase 6) and
//    clean architecture (10.1) work. SOLID is the foundation of it all.
class OrderService {
  constructor(private readonly gateway: PaymentGateway) {}
  processOrder(order: Order) { return this.gateway.pay(order.amount); }
}

Notice how everything connects {Để ý mọi thứ kết nối}: DI, repository/service, clean architecture, and testability are all consequences of SOLID {DI, repository/service, clean architecture, và khả năng test đều là hệ quả của SOLID}.


11. Capstone project {Dự án tổng kết}

Bring the whole series together — build one production-grade system {Ghép cả series lại — dựng một hệ thống đạt chuẩn production}:

A multi-user content platform (e.g. blog or project tool) with {Một nền tảng nội dung đa người dùng với}: clean layered architecture (10.1); auth with JWT + RBAC (Phase 5); PostgreSQL via a repository layer with migrations and indexes (Phase 4); Redis caching + a Bull queue for emails (Phase 6); Winston structured logging (Phase 6); validated config (Phase 7); 80%+ test coverage with unit + integration + E2E (Phase 9); event-driven side effects via an event bus (10.3); Dockerized with Compose and a GitHub Actions CI/CD pipeline (Phase 7); and load-tested with documented p99 numbers (Phase 8) {…}.

Then extract one bounded context (e.g. notifications) into a separate microservice behind an API gateway (10.2), communicating via events — to feel the real trade-offs firsthand {Rồi tách một bounded context (vd thông báo) thành microservice riêng sau API gateway (10.2), giao tiếp qua sự kiện — để cảm nhận trực tiếp các đánh đổi thật}.

More project ideas by level {Thêm ý tưởng dự án theo cấp}: Intermediate — e-commerce API, real-time chat (Socket.io), CMS; Advanced — streaming platform, analytics dashboard, message-queue system, GraphQL API; Super senior — multi-tenant SaaS, real-time collaboration tool, distributed system with eventual consistency {Trung cấp — API e-commerce, chat real-time, CMS; Nâng cao — nền tảng streaming, dashboard phân tích, hệ message-queue, GraphQL API; Super senior — SaaS đa tenant, công cụ cộng tác real-time, hệ phân tán nhất quán cuối}.


You made it — the Super Senior mindset {Bạn đã tới đích — tư duy Super Senior}

Ten phases, from the event loop to enterprise architecture {Mười phase, từ event loop tới kiến trúc enterprise}. But the real mark of a super senior isn’t memorized APIs — it’s judgment {Nhưng dấu hiệu thật của super senior không phải API thuộc lòng — mà là phán đoán}:

  • You build production-grade apps without constantly reaching for docs {Bạn dựng app chuẩn production mà không liên tục tra docs}.
  • You make informed trade-offs — SQL vs NoSQL, sessions vs JWT, monolith vs microservices — and can defend them {Bạn đánh đổi có hiểu biết — và bảo vệ được}.
  • You optimize after measuring, secure by default, and test so you can change fearlessly {Bạn tối ưu sau khi đo, bảo mật mặc định, và test để thay đổi không sợ}.
  • You know when not to use a pattern — the rarest senior skill of all {Bạn biết khi nào không dùng một pattern — kỹ năng senior hiếm nhất}.
  • You mentor others, lead technical discussions, and make architectural decisions {Bạn dìu dắt người khác, dẫn dắt thảo luận kỹ thuật, và ra quyết định kiến trúc}.

The curriculum’s closing note holds {Lời kết của curriculum vẫn đúng}: this path requires discipline and consistent practice. Focus on depth, not just breadth {con đường này đòi hỏi kỷ luật và thực hành đều đặn. Tập trung chiều sâu, không chỉ chiều rộng}. Reading these ten phases makes you knowledgeable {Đọc mười phase này khiến bạn có kiến thức}. Building the projects in every phase is what makes you a Super Senior {Dựng các dự án trong từng phase mới khiến bạn thành Super Senior}.

Now go build something real {Giờ hãy đi dựng thứ gì đó thật}. 🚀