Skip to content
DevRecall
← Roadmap
19

Monolith → Microservices

drafted

Monolith vs modular monolith vs microservices, DDD bounded contexts, strangler fig, backward compatibility & contract testing, database-per-service, sync vs async, API gateway/BFF, service mesh, tracing, distributed monolith.

0/47 rated · mastery 0%

Bài học

11 bài · ~128 phút đọc. Học theo thứ tự, rồi làm câu hỏi.
  1. 01~13 phút· 3 câuverify

    Monolith, modular monolith và microservices

    Ba kiểu kiến trúc giải quyết ba bài toán khác nhau: cái giá thật của một network boundary (availability nhân dồn, latency, mất ACID), cách giữ ranh giới cứng trong một modular monolith, khi nào không nên dùng microservices, và tiêu chí đặt một capability mới vào monolith hay service riêng.

  2. 02~13 phút· 5 câuverify

    Bounded context, Conway's law và tìm ranh giới service

    Bounded context và ubiquitous language trong DDD, context map, cách tìm ranh giới trong một monolith legacy (event storming, quyền ghi dữ liệu, change coupling từ git log), Conway's law và inverse Conway, tổ chức 5 team cho 25 service, và cách kể câu chuyện decomposition của chính bạn.

  3. 03~11 phút· 3 câuverify

    Database-per-service và query dữ liệu xuyên service

    Database-per-service nghĩa là gì (và không bắt buộc là gì), vì sao shared database là API ngầm phá deploy độc lập, cách enforce bằng schema + role trong Postgres, query xuyên service bằng API composition hay CQRS read model, xử lý read model chậm vài giây, báo cáo từ năm service, và tách một bảng `customers` dùng chung không downtime.

  4. 04~11 phút· 5 câuverify

    Strangler fig và anti-corruption layer

    Migrate một monolith đang chạy mà không big-bang: facade route dần từng capability, chọn service đầu tiên, chuyển traffic theo tenant với kill switch, shadow traffic để so sánh kết quả, rollback cả routing lẫn dữ liệu, và anti-corruption layer gom mọi phép dịch dữ liệu legacy (mã trạng thái, CHAR có padding, giờ địa phương) vào một chỗ có test.

  5. 05~11 phút· 4 câuverify

    Đồng bộ dữ liệu khi migrate: CDC, outbox và một owner duy nhất

    Giữ dữ liệu của service mới và legacy đồng bộ trong suốt strangler migration: CDC đọc WAL (chạy thật với logical decoding của Postgres), transactional outbox với relay SKIP LOCKED, vì sao dual write không atomic, source of truth theo từng giai đoạn, sự cố hai writer cùng ghi status, chuyển ownership theo tenant, reconciliation, và cách kể sự cố production khó nhất.

  6. 06~11 phút· 5 câuverify

    Backward compatibility, versioning và expand/contract

    Thay đổi nào của HTTP API và event là an toàn, thay đổi nào breaking (kể cả thêm enum), tolerant reader và vì sao `.strict()` làm vỡ consumer (chạy thật với zod 4), các chiến lược version (URL, header, evolution), deprecation có số liệu, expand/contract cho schema và API dưới rolling deploy (chạy thật trên Postgres), và quy trình sau sự cố đổi tên field.

  7. 07~11 phút· 3 câuverify

    Contract testing và chiến lược test cho microservices

    Consumer-driven contract với Pact (chạy thật: consumer test sinh pact, provider verify pass rồi fail khi đổi tên field), can-i-deploy, contract cho event, phát hiện breaking change trong OpenAPI bằng oasdiff (chạy thật, kể cả thêm enum), chiến lược test cho 20 service và luồng async qua Kafka, và cách đặt chuẩn API/event cho nhiều team mà không thành nút cổ chai.

  8. 08~12 phút· 4 câuverify

    Giao tiếp sync vs async và resilience trên mỗi call

    Khi nào gọi đồng bộ, khi nào phát event; temporal coupling và availability nhân dồn; sửa một handler Express không timeout, không kiểm tra `r.ok`, gọi tuần tự (chạy thật: 200/200 request treo khi dependency kẹt), circuit breaker với opossum, N+1 qua network trong BFF (2282 ms → 25 ms), và cách điều tra khi p99 tăng gấp đôi sau khi tách service.

  9. 09~12 phút· 5 câuverify

    API gateway, BFF, service discovery và service mesh

    Gateway làm gì và không nên làm gì, BFF cho từng loại client và cách giữ domain logic ra khỏi BFF, service discovery với Kubernetes Service/DNS và vì sao gRPC long-lived connection phá load balancing, service mesh (sidecar, ambient) khi nào đáng, retry ở app cộng mesh nhân tải (đo thật: 27 request cho 1), và authentication ở edge + authorization trong từng service (chạy thật: BOLA, spoofed header, audience sai).

  10. 10~12 phút· 4 câuverify

    Distributed tracing, health probes và twelve-factor

    Trace, span và W3C `traceparent`; OpenTelemetry trong Node (chạy thật: `fetch` không được propagate nếu thiếu instrumentation undici; inject/extract qua header Kafka), head vs tail sampling; liveness/readiness/startup probe và vì sao liveness không được check DB; twelve-factor cho service Node; graceful shutdown dưới Kubernetes (chạy thật: keep-alive giữ server sống thêm 5 giây); và debug lỗi chỉ xảy ra trên một số pod sau deploy.

  11. 11~12 phút· 6 câuverify

    Distributed monolith, platform và đo hiệu quả migration

    Distributed monolith: triệu chứng đo được và cách thoát; shared library nên chứa gì và không nên chứa gì; khi nào gộp service lại và làm thế nào; những năng lực platform phải có trước khi chạy nhiều service; đo migration bằng DORA và số deploy phải phối hợp (chạy thật trên một deploy log); và cách trả lời câu hỏi hồi cứu "bạn sẽ làm khác điều gì".

Tóm tắt & cheat sheet của track

TL;DR

  • Microservices là cách tổ chức hệ thống quanh khả năng deploy và sở hữu độc lập của từng team — không phải mục tiêu tự thân. Cái giá: mọi function call thành network call, transaction thành saga, debug cần tracing.
  • Thứ tự hợp lý thường là monolith → modular monolith → tách service khi có lý do rõ (scale khác nhau, team độc lập, release cadence, isolation lỗi/compliance).
  • Ranh giới service đến từ bounded context (DDD) và dữ liệu mà service sở hữu (database-per-service). Ranh giới sai → distributed monolith: tất cả nhược điểm của cả hai.
  • Migration an toàn = strangler fig (facade route dần), anti-corruption layer, đồng bộ dữ liệu (CDC/outbox), backward compatibility (expand/contract, tolerant reader, contract test), đo parity trước khi cắt.
  • Hạ tầng tối thiểu trước khi tách: CI/CD mỗi service, observability (log có correlation id, metrics, distributed tracing), config/secrets, service discovery, gateway.

Khái niệm cốt lõi

Khái niệmMột câuVí dụ ngắn
Modular monolithMột deploy unit, module có ranh giới cứng (API nội bộ, không truy cập DB của nhau)orders/, catalog/ chỉ gọi nhau qua interface
Bounded contextRanh giới nơi một model/ngôn ngữ có nghĩa nhất quán"Product" ở Catalog ≠ "Product" ở Warehouse
Database per serviceMỗi service sở hữu dữ liệu; service khác chỉ qua API/eventOrder service không JOIN bảng của Customer
Strangler figFacade phía trước legacy, chuyển dần từng route sang service mới/api/invoices/* → Billing service
Anti-corruption layerLớp dịch giữa model legacy và model mớiMap CUST_TP=3 → CustomerTier.Gold
API gatewayĐiểm vào chung: routing, auth, rate limit, TLSKong, AWS API Gateway, NGINX
BFFBackend riêng cho từng loại clientmobile-bff gom 5 call thành 1
Service discoveryTìm địa chỉ instance độngK8s Service DNS, Consul
Service meshSidecar/proxy lo mTLS, retry, traffic shifting, telemetryIstio, Linkerd
Contract testingKiểm tra provider thoả mãn kỳ vọng của consumerPact
Distributed tracingTheo dõi một request qua nhiều service bằng trace idW3C traceparent, OpenTelemetry
Expand / contractThay đổi schema/API theo 2 pha tương thíchThêm cột mới → migrate → bỏ cột cũ

Cơ chế hoạt động

Strangler fig migration

Các bước cắt một capability

Trade-offs

MonolithModular monolithMicroservices
DeployMột đơn vịMột đơn vịĐộc lập từng service
TransactionACID dễACID dễSaga, eventual consistency
Scale teamKém khi > vài teamTốt vừaTốt nếu ranh giới đúng
Vận hànhĐơn giảnĐơn giảnNặng: CI/CD, observability, network, on-call
Isolation lỗiThấpThấpCao (nếu có timeout/bulkhead)
LatencyFunction callFunction callNetwork hop mỗi call
Refactor ranh giớiDễDễ vừaĐắt (API/data migration)
Giao tiếpƯuNhượcDùng khi
Sync (REST/gRPC)Đơn giản, có kết quả ngayTemporal coupling, cascading failure, latency cộng dồnQuery cần trả lời ngay, validate
Async (event/queue)Loose coupling, chịu lỗi, fan-outEventual consistency, debug khó, cần idempotencyThông báo thay đổi, workflow dài, đồng bộ read model

Pitfalls

  • Tách theo layer kỹ thuật (auth-service, db-service, validation-service) thay vì theo capability nghiệp vụ.
  • Shared database giữa service → không deploy/đổi schema độc lập được.
  • Một user action cần gọi đồng bộ 5 service nối tiếp → availability = tích (0.99^5 ≈ 95%), latency cộng dồn.
  • Deploy phải theo thứ tự ("B trước rồi mới A") → dấu hiệu distributed monolith.
  • Shared library chứa domain model dùng chung → mọi service phải nâng version cùng lúc.
  • Breaking API/event change không có versioning, không contract test.
  • Tách service trước khi có CI/CD, tracing, dashboard → không biết lỗi ở đâu.
  • Big-bang rewrite: đóng băng feature nhiều tháng rồi cắt một lần.

Cheat sheet

  • Bắt đầu: modular monolith; tách khi có lý do đo được (team, scale, release, compliance).
  • Ranh giới: bounded context + dữ liệu sở hữu + tần suất thay đổi cùng nhau (change coupling từ git log).
  • Migration: strangler fig + ACL + CDC/outbox + shadow traffic + chuyển dần + rollback bằng routing.
  • Tương thích: thêm field optional, không đổi nghĩa/xoá field khi còn consumer; tolerant reader; expand/contract; version khi buộc phải breaking; consumer-driven contract test trong CI.
  • Data: database-per-service; query xuyên service bằng API composition hoặc read model (CQRS) cập nhật qua event.
  • Mỗi call: timeout, retry có budget, circuit breaker, correlation/trace id (traceparent).
  • Gateway: cross-cutting (auth, rate limit, routing), không chứa business logic; BFF cho nhu cầu riêng từng client.
  • Distributed monolith smell: lockstep deploy, shared DB, chatty sync call, một thay đổi chạm nhiều service.

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 47 / 47

// acl/legacyCustomer.ts
export function toCustomer(row: LegacyCustRow): Customer {
  return {
    id: row.CUST_ID.trim(),
    tier: ({ "1": "standard", "3": "gold" } as const)[row.CUST_TP] ?? "standard",
    email: row.EMAIL_ADDR?.toLowerCase() ?? null,
  };
}
rename customers.fullname -> display_name
v1: add display_name (nullable); write both; backfill
v2: read display_name; still write both
v3: stop writing fullname
v4: drop fullname
app.get("/orders/:id", async (req, res) => {
  const order = await db.order.findUnique({ where: { id: req.params.id } });
  const customer = await fetch(`${CUSTOMER_URL}/customers/${order!.customerId}`)
    .then((r) => r.json());
  const pricing = await fetch(`${PRICING_URL}/quote`, {
    method: "POST",
    body: JSON.stringify(order),
  }).then((r) => r.json());
  res.json({ ...order, customer, pricing });
});
app.get("/bff/orders", async (req, res) => {
  const orders = await ordersApi.list({ userId: req.user.id, limit: 50 });
  const result = [];
  for (const o of orders) {
    const product = await catalogApi.get(o.productId);
    const shipment = await shippingApi.get(o.id);
    result.push({ ...o, product, shipment });
  }
  res.json(result);
});
// before: { "id": "c1", "email": "a@x.io", "tier": "gold" }
// after:  { "id": "c1", "contact": { "email": "a@x.io" }, "tier": "GOLD", "segment": "b2b" }
const Customer = z.object({
  id: z.string(),
  email: z.string().email(),
  tier: z.enum(["standard", "gold"]),
}).strict();
consumer.on("CustomerUpdated", (msg) => repo.upsert(Customer.parse(msg)));