Chương 06 · Service Architecture

Microservices vs Monolith

Khi nào split, decomposition theo Bounded Context, sync vs async, Service Mesh, Circuit Breaker, distributed tracing, deployment patterns.

1. Monolith vs Microservice

Monolith

1 codebase, 1 deployment, 1 process, 1 DB.

  • (+) Đơn giản — đọc code, debug, deploy.
  • (+) Transaction ACID nội bộ.
  • (+) Refactor dễ — IDE rename hoạt động cross-module.
  • (+) Dev local: chạy 1 command.
  • (−) Codebase lớn → build/test chậm.
  • (−) Scale toàn app, không scale phần cụ thể.
  • (−) 1 bug crash toàn app.
  • (−) Khó adopt tech mới (cùng stack).

Microservice

Nhiều service nhỏ, deploy độc lập, communicate qua network.

  • (+) Scale theo service (auth heavy → 10 instance).
  • (+) Deploy độc lập (đổi recommend không động checkout).
  • (+) Tech polyglot (Go cho high perf, Python cho ML).
  • (+) Team autonomy.
  • (+) Fault isolation (1 service down không kéo cả app).
  • (−) Distributed system complexity (CAP, eventual).
  • (−) Operational overhead (monitoring, deploy, on-call).
  • (−) Network latency (RPC chậm hơn function call 1000×).
  • (−) Cross-service transaction khó (saga, không ACID).
  • (−) Cross-cutting concern (auth, logging) phức tạp hơn.

1.1. Khi nào monolith?

  • Team < 20 người.
  • Domain chưa rõ — sẽ refactor nhiều.
  • Prototype, MVP, startup early.
  • App CRUD đơn giản, không cần scale phức tạp.

1.2. Khi nào microservice?

  • Team > 50 người, nhiều team độc lập.
  • Domain stable, BC rõ.
  • Cần scale phần cụ thể (vd: video transcode tốn CPU, profile lookup nhẹ).
  • Deploy frequency cao (nhiều service deploy/day).
  • Có ngân sách operational cho phức tạp.
Quy tắc Martin Fowler "Almost all the successful microservice stories have started with a monolith that got too big and was broken up. Almost all the cases where I've heard of a system that was built as a microservice system from scratch, it has ended up in serious trouble." Bắt đầu monolith. Khi đau thật sự, mới split.

1.3. Modular Monolith — best of both?

1 deployment, nhưng code chia thành module rõ với boundaries nội bộ chặt. Không cross-module direct call — qua interface/event nội bộ. Khi cần split sau này, đã có boundaries sẵn.

monolith/
├── modules/
│   ├── billing/
│   │   ├── api/         (only this layer is "public")
│   │   ├── domain/
│   │   ├── infrastructure/
│   ├── shipping/
│   ├── inventory/
│   └── notification/
├── shared/              (only DTOs, contracts)
└── app.ts               (composition root)

Mỗi module độc lập như microservice nhưng không có network overhead. Shopify, Basecamp dùng pattern này thành công với 100k+ LOC.

2. Distributed Monolith — Anti-pattern

Worst of both worlds: chia microservice nhưng coupling chặt. Đổi 1 service = phải deploy đồng thời nhiều service.

2.1. Dấu hiệu

  • Service A gọi service B, B gọi C, C gọi D — chuỗi sync RPC.
  • Service share database (cùng table).
  • Deploy theo "release train" — tất cả service deploy cùng lúc.
  • Local dev: chạy 1 service phải bật 10 service khác.
  • Mỗi feature đụng vào 5+ service.
  • Bug bụi này thấy ở service kia (network → khó hơn function call).

2.2. Vì sao tệ hơn monolith?

  • Phức tạp microservice (network, distributed) + coupling monolith (deploy chung).
  • Latency cao (5 hop × 5ms = 25ms baseline).
  • 1 service down = chuỗi down.
  • Hard to understand — dependency graph ẩn.

2.3. Phòng tránh

  • Mỗi service có DB riêng (database-per-service).
  • Communicate ưu tiên async (event) thay sync (RPC).
  • Service boundary theo Bounded Context, không theo "tier" (web, business, data).
  • Có thể deploy 1 service mà không động service khác → contract stable.
  • Backward-compatible API — versioning.

3. Service Decomposition — chia theo nguyên lý nào?

3.1. Decompose by Business Capability

Mỗi service phục vụ 1 business capability rõ.

  • User Management.
  • Order Processing.
  • Inventory Management.
  • Shipping.
  • Billing & Payment.
  • Notification.

3.2. Decompose by Subdomain (DDD)

Đã học OOP Ch8 — Bounded Context. Mỗi BC có model riêng → service riêng tự nhiên.

3 loại subdomain:

  • Core domain — competitive advantage (Uber: matching/routing). Đầu tư nhiều.
  • Supporting domain — quan trọng nhưng không khác biệt (billing). Có thể outsource.
  • Generic domain — commodity (auth). Dùng SaaS (Auth0, Okta).

3.3. Decompose by Strangler Fig Pattern

Migrate monolith → microservice gradually:

  1. Đặt API Gateway trước monolith.
  2. Identify 1 capability (vd: notification) → tạo service mới.
  3. Gateway route notification request đến service mới, còn lại đến monolith.
  4. Khi xong, monolith không còn code notification.
  5. Lặp lại cho capability khác.
  6. Cuối cùng, monolith "biến mất", chỉ còn microservice.

Tên "strangler fig" từ cây ficus thắt — lớn dần bóp chết cây chủ. Pattern Martin Fowler.

3.4. Anti-pattern decomposition

  • Tier-based split — "frontend service", "business service", "data service". Đây là 3-tier monolith được rải qua network. KHÔNG.
  • Entity-based split — "User Service", "Order Service" — đôi khi đúng nhưng đôi khi tạo coupling chặt vì 2 entity gắn chặt nghiệp vụ.
  • Too small (nano-service) — 1 service cho 1 endpoint. Network overhead lớn hơn benefit.

3.5. Service size sweet spot

Quy tắc Amazon "two-pizza team" — 1 service do 1 team 6-8 người maintain. Không phải dòng code, mà là cognitive load.

Ngon nhất: service "đủ nhỏ để hiểu, đủ lớn để làm việc có ý nghĩa". Thường 5-50k LOC.

4. Inter-service Communication

4.1. Sync — REST

GET /api/v1/users/123
Host: user-service.local:8080
Accept: application/json

# Response
HTTP/1.1 200 OK
Content-Type: application/json
{ "id": 123, "name": "Alice" }

(+) Universal, debugging tools nhiều, dễ học.

(−) JSON parsing overhead, không type-safe.

4.2. Sync — gRPC

HTTP/2 + Protobuf. Nhanh hơn REST 5-10×, type-safe, streaming support.

// user.proto
service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc StreamUsers(StreamRequest) returns (stream User);
}

message GetUserRequest { int64 id = 1; }
message User { int64 id = 1; string name = 2; }

(+) Performance, type-safe, code gen client/server.

(−) Browser support limited (cần grpc-web), debugging khó hơn (binary), learning curve.

4.3. Sync — GraphQL

1 endpoint, client request schema cụ thể. Tốt cho frontend BFF (Backend For Frontend).

Trong inter-service: hiếm dùng (REST/gRPC phổ biến hơn). GraphQL Federation có thể.

4.4. Async — Event/Message

Đã học Ch5. Service publish event, các service khác subscribe. Loose coupling.

4.5. Sync vs Async — chọn cái nào?

Tình huốngPhù hợp
User chờ response (login, create order)Sync (REST/gRPC)
Background work (email, transcode)Async (queue)
Cross-service notification (order created → email)Async (event)
Real-time data (chat, live update)WebSocket / Server-Sent Events
Service-to-service query (cần data thật từ service khác)Sync
Audit, analytics, logAsync (fire and forget)

4.6. Pattern: Async-first với Sync fallback

Mặc định async; chỉ sync khi user thực sự cần response immediate. Giảm coupling, tăng resilience.

5. Service Mesh

Khi có 50+ service, vấn đề "cross-cutting" lặp ở mọi service:

  • mTLS giữa service.
  • Retry/timeout/circuit breaker.
  • Load balancing.
  • Tracing & metrics.
  • Authn/Authz.

Service Mesh tách những concern này khỏi app code, đặt vào sidecar proxy đứng cạnh mỗi service.

Mỗi pod / service = App container + Sidecar (proxy) Pod 1 Pod 2 ┌───────────────┐ ┌───────────────┐ │ App │ │ App │ │ ▼ localhost │ │ ▲ localhost │ │ Sidecar │ ◄────►│ Sidecar │ │ (Envoy proxy) │ mTLS │ (Envoy proxy) │ └───────────────┘ └───────────────┘ ▲ ▲ │ Control plane │ └────── Istiod ─────────┘ (config, policy, telemetry)

5.1. Tools

  • Istio — Google, phổ biến nhất, feature đầy đủ, complex.
  • Linkerd — Buoyant, simpler, light, fast.
  • Consul Connect — HashiCorp.
  • AWS App Mesh — managed.

5.2. Trade-off

  • (+) App code không cần biết về retry/mTLS/tracing.
  • (+) Policy uniform: thay 1 file YAML, áp dụng toàn cluster.
  • (+) Observability tự động: mọi traffic đi qua sidecar.
  • (−) Latency overhead 1-3ms per hop (sidecar proxy).
  • (−) Operational complexity (control plane, sidecar lifecycle).
  • (−) Hardware overhead — sidecar tốn CPU/RAM.

Khi nào? > 30 microservice. Dưới đó, library-based (Resilience4j, Polly) đủ.

6. Resilience Patterns

6.1. Timeout

Đặt timeout cho mọi external call. Default không bao giờ infinite.

// ❌ No timeout — request có thể hang vô hạn
const result = await fetch(url);

// ✓ Timeout 5s
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);
const result = await fetch(url, { signal: controller.signal });
clearTimeout(id);

Quy tắc: timeout chuỗi service phải giảm dần. Edge 30s, gateway 25s, service 20s, DB 15s. Tránh "outer chờ inner đã chết".

6.2. Retry với Exponential Backoff

async function withRetry<T>(fn: () => Promise<T>, max = 3): Promise<T> {
  for (let i = 0; i < max; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === max - 1) throw err;
      if (!isRetryable(err)) throw err;   // không retry 400, chỉ 5xx + network error
      const delay = Math.min(1000 * Math.pow(2, i) + Math.random() * 500, 30_000);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('unreachable');
}

Quy tắc:

  • Chỉ retry idempotent operation (GET, PUT, DELETE) — không POST trừ khi có idempotency key.
  • Backoff exponential + jitter — tránh retry storm.
  • Cap max retry (3-5) — không vô hạn.
  • Chỉ retry retryable error — 5xx, network error. KHÔNG retry 4xx.

6.3. Circuit Breaker

Kill request đến service đang fail. 3 state:

Closed (normal) │ │ failure rate > threshold (vd: 50% trong 30s) ▼ Open (block calls) │ │ sau timeout (vd: 30s) ▼ Half-Open (thử 1 call) │ ├─ success → Closed └─ fail → Open
  • Closed — gọi bình thường, đếm fail.
  • Open — fail rate cao → mở mạch, mọi call fail ngay (không gọi service đang chết).
  • Half-Open — sau timeout, thử 1 call để xem service đã recover.

Lợi: tránh overload service đang chết, fail-fast cho user.

Tools: Resilience4j (Java), Polly (.NET), Hystrix (deprecated), opossum (Node.js), Istio circuit-breaking config.

6.4. Bulkhead

Tách isolation — 1 service down không kéo phần khác.

Ví dụ: separate thread pool / connection pool cho mỗi downstream:

// ❌ Shared pool — service A chậm → user request không xử lý được
const sharedPool = new Pool({ max: 100 });

// ✓ Bulkhead — pool riêng cho mỗi downstream
const userServicePool = new Pool({ max: 50 });
const orderServicePool = new Pool({ max: 30 });
const billingServicePool = new Pool({ max: 20 });

Tên "bulkhead" từ tàu thủy — vách ngăn để 1 khoang ngập không kéo chìm tàu.

6.5. Fail-fast vs Fail-safe

  • Fail-fast: detect lỗi sớm, throw ngay (timeout, circuit breaker).
  • Fail-safe: degrade gracefully — trả default value, fallback cache, partial response.

Vd: recommendation service down → trả top sản phẩm hot thay vì error 500. UX tốt hơn.

7. Distributed Tracing

1 request đi qua 5 service. Service C trả 500. Làm sao biết B → C call đã có vấn đề từ A → B?

7.1. Trace, Span, Context

  • Trace — toàn bộ "đường đi" của 1 request.
  • Span — 1 đoạn (vd: A xử lý request, B xử lý request, DB query).
  • Trace context — IDs propagate qua tất cả service: trace-id, span-id, parent-span-id.
Request: "GET /order/123" Trace ID: abc-123 (unique cho request này) ├─ Span 1: API Gateway [50ms total] │ ├─ Span 2: Auth check [5ms] │ ├─ Span 3: Order Service [40ms] │ │ ├─ Span 4: DB query [10ms] │ │ └─ Span 5: Inventory [25ms] │ │ └─ Span 6: Redis [5ms] │ └─ Span 7: Response build [3ms]

7.2. OpenTelemetry — chuẩn

Standard library cho tracing/metrics/logs. Vendor-neutral. Backend export (Jaeger, Tempo, Datadog, Honeycomb).

import { trace, context } from '@opentelemetry/api';

const tracer = trace.getTracer('order-service');

async function getOrder(id: string) {
  return tracer.startActiveSpan('getOrder', async (span) => {
    span.setAttribute('order.id', id);
    try {
      const order = await db.query('SELECT...', [id]);
      span.setStatus({ code: 1 });   // OK
      return order;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: 2 });   // ERROR
      throw err;
    } finally {
      span.end();
    }
  });
}

7.3. Sampling

Trace mọi request quá tốn — sample. Strategies:

  • Head sampling — quyết định ở entry, vd: 1% mọi request.
  • Tail sampling — đợi xong rồi quyết định lưu. Lưu mọi request slow + error, sample 1% successful.
  • Probabilistic — random N%.
  • Rate-limited — max 10 trace/s mỗi service.

7.4. 3 pillars of observability

  1. Logs — text events. Khi query "what happened at 14:23".
  2. Metrics — numeric time-series. "error rate, latency, QPS" — Prometheus.
  3. Traces — request flow cross-service. Jaeger, Tempo.

Logs đắt nhất, metrics rẻ nhất. Trace cân bằng. Tốt nhất: cả 3 với common correlation ID.

8. Deployment Patterns

8.1. Blue-Green Deployment

2 environment giống nhau:

  • Blue = production hiện tại.
  • Green = version mới deployed.

Test green xong → switch traffic blue → green qua LB. Sự cố → switch ngược ngay.

(+) Rollback dưới giây. (−) Tốn 2× hardware lúc deploy.

8.2. Canary Deployment

Route N% (5%) traffic đến version mới. Monitor metrics. OK → tăng dần (10%, 25%, 50%, 100%). Lỗi → rollback.

Tốt hơn blue-green vì phát hiện bug với traffic thực dần dần.

8.3. Rolling Deployment

Replace instance từng ít một. 10 instance → kill 2, deploy 2 new, repeat. Kubernetes default.

(+) Không tốn thêm hardware. (−) Có thời gian cùng tồn tại 2 version → API phải backward-compatible.

8.4. Feature Flags

Deploy code có feature mới nhưng wrap trong flag. Flag off → behavior cũ. Flag on → behavior mới.

if (await featureFlag.isEnabled('new-checkout-flow', { userId })) {
  return newCheckout(req);
}
return oldCheckout(req);

Lợi:

  • Decouple deploy với release — code đã prod nhưng off.
  • A/B test — bật cho 10% user.
  • Kill switch — feature buggy → tắt ngay không cần redeploy.
  • Gradual rollout — bật theo region, theo user segment.

Tools: LaunchDarkly, Split.io, Unleash, GrowthBook (open-source). Hoặc DIY với DB/Redis flag.

8.5. Shadow Deployment

Deploy version mới, mirror traffic — không trả response cho user. Compare output với version cũ. Phát hiện bug trước khi thực sự switch.

8.6. CI/CD pipeline cơ bản

  1. Code → git push.
  2. CI: run test (unit + integration).
  3. Build image (Docker).
  4. Deploy staging → smoke test.
  5. Deploy canary 5% prod.
  6. Auto-monitor 30 phút.
  7. Auto-promote 100% nếu metric OK; rollback nếu fail.

9. Bài tập

  1. Đánh giá: project bạn đang làm phù hợp Monolith, Modular Monolith, hay Microservice? Lý giải 3 yếu tố cụ thể.
  2. Cho domain e-commerce, đề xuất 5-7 microservice theo Bounded Context. Vẽ dependency graph + xác định async vs sync giữa các service.
  3. Implement Circuit Breaker đơn giản (in-memory) cho HTTP client: 50% fail trong 30s → open, sau 30s thử lại. Test với mock service đôi khi return 500.
  4. Setup distributed tracing local: 3 service Node.js + Jaeger. Trace 1 request qua chain. Quan sát Jaeger UI.
  5. Phát hiện distributed monolith: cho 4 service, mỗi service dùng chung 1 PostgresDB và gọi nhau qua REST chain. Đề xuất refactor.
  6. Thiết kế feature flag system đơn giản: bật flag "new-search" cho 10% user dựa hash userId. Có thể tăng dần lên 100% qua dashboard.
  7. Canary deployment với nginx: route 5% traffic v2, 95% v1. Auto-rollback khi error rate v2 > 2%.
  8. Khi nào dùng gRPC thay REST cho service-to-service? Cho 2 ví dụ cụ thể.

10. Quiz

Quiz cuối Chương 6

Distributed Monolith là:

  • Monolith chạy trên nhiều server
  • Microservice tốt
  • Worst-of-both: chia microservice nhưng coupling chặt — share DB, deploy chung, sync chain dài; phức tạp distributed + cứng monolith
  • Pattern khuyến nghị
Anti-pattern phổ biến khi team mới microservice. Dấu hiệu: deploy theo "release train" tất cả service cùng lúc, share DB, local dev cần chạy 10 service, mỗi feature đụng 5+ service. Phòng tránh: DB-per-service, async communication, BC-based decomposition.

Martin Fowler khuyên về microservice:

  • Bắt đầu microservice ngay từ đầu
  • Bắt đầu monolith. Khi đau thật sự, mới split microservice — mọi success story đều là monolith→microservice, đa số "microservice from scratch" thất bại
  • Microservice cho mọi project
  • Tránh microservice hoàn toàn
"Almost all the successful microservice stories have started with a monolith that got too big and was broken up." Khi domain chưa rõ, ranh giới chưa chắc — split sai → migrate đau gấp đôi. Modular monolith là middle-ground tốt.

Circuit Breaker pattern có 3 state:

  • On, Off, Standby
  • Active, Inactive, Test
  • Start, Run, Stop
  • Closed (normal), Open (block calls khi service fail), Half-Open (thử 1 call sau timeout để check recovery)
Closed: gọi normal, đếm fail. Open: fail rate > threshold → block, fail-fast. Half-Open: timeout (vd 30s) → thử 1 call. Success → Closed; Fail → Open. Tránh overload service đang chết. Tools: Resilience4j, Polly, Istio.

Bulkhead pattern:

  • Tách isolation — pool/thread riêng cho mỗi downstream để 1 service down không kéo phần khác
  • Tăng throughput
  • Cache
  • Encryption
Tên từ tàu thủy — vách ngăn 1 khoang ngập không chìm tàu. Khi service A chậm, request đến A chiếm hết shared pool → toàn app block. Giải: separate connection pool per downstream. Resilience4j Bulkhead, Hystrix isolation strategy.

Service Mesh (Istio, Linkerd) tách:

  • DB
  • Frontend
  • Cross-cutting concern (mTLS, retry, tracing, LB) khỏi app code, đặt vào sidecar proxy bên cạnh mỗi service
  • Storage
App talk localhost với sidecar (Envoy). Sidecar handle mTLS, retry, circuit breaker, telemetry. Policy uniform qua YAML config. Trade-off: latency 1-3ms per hop, complexity. Phù hợp khi > 30 microservice; dưới đó library (Resilience4j, Polly) đủ.

"Strangler Fig Pattern" cho migrate monolith:

  • Big bang rewrite
  • Tắt monolith
  • Replace tất cả 1 lần
  • Đặt API Gateway trước monolith → tách từng capability ra service mới → gateway route từng phần — gradual
Tên từ cây ficus thắt — lớn dần bóp chết cây chủ. Pattern Martin Fowler. Mỗi sprint tách 1 capability (notification, billing...) thành service mới. Gateway route. Cuối cùng monolith "biến mất". Risk thấp hơn rewrite.

Distributed tracing core concept:

  • Lưu log vào nhiều DB
  • Trace ID + Span ID propagate qua mọi service trong 1 request → reconstruct flow cross-service
  • Thay log
  • Encryption
Trace = full request path. Span = 1 đoạn (1 service xử lý). Trace context (trace-id, span-id, parent) propagate qua HTTP header, gRPC metadata, message header. OpenTelemetry là chuẩn. Backend: Jaeger, Tempo, Honeycomb.

Feature flags decouple:

  • Deploy code và release feature — code đã prod, flag off; bật cho subset user, A/B test, kill switch khi buggy
  • Frontend và backend
  • DB schema
  • Cache
Pattern hiện đại quan trọng. Deploy code mỗi tuần, release feature theo schedule riêng. Bật cho 1% user, monitor, tăng dần. Bug? Tắt flag dưới giây không cần redeploy. Tools: LaunchDarkly, Split, Unleash, GrowthBook.

Hoàn thành Chương 6. Tiếp theo: Chương 7 — CDN, Rate Limiting, API Gateway, Security →