Capstone — thiết kế một hệ dữ liệu phái sinh đầu-cuối
Đồ án tổng hợp cả track: thiết kế một hệ dữ liệu thật nối source of truth, CDC, stream, batch và các read model phái sinh — lập luận trade-off ở từng mắt xích.
OLShop là sàn thương mại điện tử đang xử lý 800.000 đơn hàng mỗi ngày. Team data vừa phát hiện ba hệ thống đang vận hành trong silo: PostgreSQL OLTP (source of record cho đơn hàng và tồn kho), Elasticsearch (search và đề xuất sản phẩm), Redis (tồn kho real-time cho giỏ hàng). Mỗi hệ thống tự cập nhật theo cách riêng — và sau một sự cố dual-write bị crash giữa chừng, kho trên ứng dụng lệch khỏi kho thực tế 12.000 sản phẩm mà không ai biết cho đến khi khách hàng đặt đơn cho sản phẩm đã hết hàng.
Đây là bài tổng hợp cả track: bạn kết nối input bất biến từ Module 1, stream processing từ Module 2, và kiến trúc phái sinh từ Module 3 thành một hệ thống vận hành hoàn chỉnh. fadeLevel: independent có nghĩa bạn phải tự thiết kế toàn bộ trước khi đọc gợi ý.
1. 🎯 Đề bài
Bạn được giao thiết kế hệ dữ liệu phái sinh đầu-cuối cho OLShop, thỏa mãn đồng thời ba nhóm yêu cầu:
Nhóm A — Đồng bộ real-time (latency tối đa 1 giây):
- Tồn kho trên Redis cập nhật ngay khi đơn hàng được đặt hoặc hủy — không thể để dual-write crash
- Elasticsearch được cập nhật để phản ánh tên và giá sản phẩm mới nhất
Nhóm B — Báo cáo chính xác (batch hàng ngày):
- Báo cáo doanh thu tháng, top sản phẩm bán chạy theo danh mục
- Idempotent: chạy lại bất kỳ lúc nào cũng cho kết quả giống nhau
Nhóm C — Lịch sử và audit:
- Domain inventory phải có khả năng replay lại toàn bộ lịch sử thay đổi tồn kho từ đầu
- Lịch sử đơn hàng phải truy vấn được nhanh, tách biệt với luồng đặt hàng đang chạy
Bốn việc bạn cần tự làm:
(a) Vẽ kiến trúc đầu-cuối — từ PostgreSQL OLTP (source of record) qua cơ chế phát changelog đến từng derived view. Mỗi mũi tên ghi rõ: cơ chế gì, đảm bảo gì (at-least-once / exactly-once / idempotent), thành phần nào là source of truth.
(b) Chọn cơ chế phát changelog và lập luận theo nguy cơ dual-write: kịch bản nào cụ thể gây bất nhất nếu chọn sai, và cơ chế nào ngăn được tại nguồn.
(c) Chọn event sourcing hay CQRS cho domain inventory và lập luận: vì sao domain này cần lịch sử bất biến? Khi nào cần snapshot, ngưỡng bao nhiêu?
(d) Chọn Lambda hay Kappa và lập luận định lượng: với 800.000 đơn/ngày, tính Kafka storage cần bao nhiêu GB nếu giữ đủ để reprocess, rồi so sánh với phương án Lambda dùng S3 làm batch layer.
2. 🔍 I-P-O
| Chi tiết | |
|---|---|
| Source of record | PostgreSQL OLTP: bảng orders + order_items (800K đơn/ngày), bảng products (5M sản phẩm) |
| Process | Changelog → stream real-time + batch snapshot → derived views |
| Derived views | view tồn kho real-time, view search sản phẩm, view báo cáo doanh thu, audit lịch sử thay đổi inventory |
| Ràng buộc | Mọi derived view phải recomputable từ source of record; không có dual-write |
3. 📦 Concept mapping
Bài capstone này là bằng chứng trực tiếp cho cả bốn learning outcome của module và nối cả track:
- LO1 — Explain CDC (bài 01 — CDC): yêu cầu (a) và (b) buộc bạn chọn cơ chế phát changelog phù hợp và lập luận theo nguy cơ dual-write — kịch bản nào gây bất nhất và cơ chế nào ngăn tại nguồn.
- LO2 — Compare Event Sourcing và CQRS (bài 02, bài 03): yêu cầu (c) buộc bạn chọn pattern phù hợp với domain inventory — lưu chuỗi event bất biến hay tách read/write model, và khi nào cần snapshot.
- LO3 — Compare Lambda và Kappa (bài 04): yêu cầu (d) buộc bạn tính toán định lượng chi phí storage rồi chọn kiến trúc dựa trên số liệu thực tế của OLShop.
- LO4 — Design đầu-cuối (chính bài này): tổng hợp cả ba module thành một pipeline hoàn chỉnh.
Nối cả track:
- Module 1 — Batch: batch report là derived data bất biến từ snapshot OLTP — áp nguyên tắc input bất biến và idempotent replay.
- Module 2 — Stream: CDC changelog chạy vào stream processor để cập nhật Redis và Elasticsearch real-time; windowing và watermark từ Module 2 áp cho anomaly detection.
- Module 3 — Kiến trúc phái sinh: CDC, Event Sourcing, CQRS, Lambda/Kappa là bốn mắt xích cùng tồn tại trong một hệ thống.
4. ▶️ Starter — tự thiết kế trước
Làm xong template này TRƯỚC khi đọc Gợi ý. Thiết kế sai ban đầu là bình thường — điều quan trọng là lý luận đằng sau từng lựa chọn. Gợi ý chỉ đặt câu hỏi định hướng, không lộ kết luận hay kiến trúc cụ thể — toàn bộ ở §6.
Thay mọi [TODO] bằng thiết kế của bạn. Comment sau -- là nơi ghi lý luận.
SYSTEM olshop_derived_data():
-- =========================================================
-- TANG 1: SOURCE OF RECORD + CHANGELOG
-- =========================================================
-- PostgreSQL OLTP: bang orders, order_items, products
-- Van de: Redis + Elasticsearch can biet khi co thay doi
-- ma khong phu thuoc vao app goi truc tiep
-- Chon co che phat changelog:
-- (A) dual-write: app ghi DB xong, goi Redis/ES truc tiep
-- (B) WAL-based CDC: doc transaction log PostgreSQL (WAL)
-- (C) Outbox pattern: ghi event vao bang outbox cung transaction
-- Chon: [TODO: A / B / C]
-- Ly do: [TODO -- neu crash giua 2 buoc cua (A), dieu gi xay ra?
-- -- neu them retry vao (A), nguy co moi nao xuat hien?]
-- Neu chon (B) hoac (C):
cdc_events <- read_changelog([TODO: doc tu WAL hay bang outbox?])
.publish("orders-changelog",
key = [TODO: key gi? anh huong den thu tu event?])
.publish("inventory-changelog",
key = [TODO])
-- (C) Outbox khac (B) WAL-CDC o tang nao? [TODO: 1 cau — infrastructure vs application]
-- Khi nao khong the dung (B)? [TODO]
-- =========================================================
-- TANG 2: STREAM PROCESSING (Module 2 recall)
-- =========================================================
-- Real-time: cap nhat ton kho Redis, cap nhat Elasticsearch,
-- phat hien don bat thuong (5 don / 1 account / 1 phut)
order_stream <- kafka_source("orders-changelog")
.assign_event_time(e -> [TODO: truong nao?])
.assign_watermark(max_event_time - [TODO: delay bao nhieu?])
-- Consumer A: cap nhat ton kho Redis (idempotent)
inventory_delta <- order_stream
.filter(e -> [TODO: chi su kien nao anh huong ton kho?])
.key_by(e -> e.product_id)
inventory_delta.sink_to("redis://inventory",
delivery = [TODO: at_least_once / exactly_once],
-- Neu cap nhat la "delta += qty" va message den 2 lan:
-- [TODO: van de gi? giai phap de idempotent?])
-- Consumer B: phat hien don bat thuong
anomaly <- order_stream
.key_by(e -> [TODO: key gi? tai sao?])
.window([TODO: window type va duration])
.aggregate([TODO: ham gi?])
.filter(r -> [TODO: dieu kien bat thuong?])
anomaly.sink_to_kafka("anomaly-alerts", delivery = [TODO])
-- Consumer C: cap nhat Elasticsearch order index
order_stream.upsert_to("elasticsearch://orders-index",
key = [TODO],
delivery = [TODO])
-- =========================================================
-- TANG 3: BATCH PROCESSING (Module 1 recall)
-- =========================================================
-- Accurate: bao cao doanh thu thang, top san pham theo danh muc
-- Input: [TODO: doc truc tiep OLTP hay tu snapshot bat bien?]
-- Tai sao khong query truc tiep OLTP?
-- [TODO: 800K don/ngay x 30 ngay tren bang orders dang serve web traffic, van de gi?]
batch_monthly_report(month):
orders_snap <- [TODO: tu dau? BAT BIEN trong suot job]
products_snap <- [TODO: tu dau?]
step1 <- orders_snap
.filter(o -> [TODO: dieu kien loc] AND o.month = month)
.project(o -> [TODO: chi giu field nao cho join + aggregate?])
write("work/step1/" + month + "/", step1)
-- Uoc tinh kich thuoc step1: [TODO] GB (vi sao?)
step2 <- join(step1, products_snap,
key = [TODO],
strategy = [TODO: broadcast / sort-merge -- ly do?])
-- step1 ~ [TODO] GB, products_snap ~ 500 MB
-- Broadcast threshold mac dinh: 10 MB; hard limit: 8 GB x C worker
write("work/step2/" + month + "/", step2)
step3 <- step2.group_by([TODO: group key?]).aggregate(SUM([TODO]))
write("work/step3/" + month + "/", step3)
result <- step3.rank_per_group(n = 10)
write("report/" + month + "/", result)
-- Chay lai 3 lan cho ket qua giong nhau khong? [TODO: co/khong, ly do]
-- =========================================================
-- TANG 4: DOMAIN INVENTORY -- chon pattern luu tru
-- =========================================================
-- Yeu cau (nhom C): inventory phai REPLAY lai toan bo lich su thay doi ton kho tu dau
-- Pattern nao phu hop? [ ] Event Sourcing [ ] CQRS [ ] CRUD thuong [ ] ket hop
-- Ly do (dua vao yeu cau "replay lich su tu dau"): [TODO]
-- Neu pattern ban chon dung chuoi thay doi bat bien:
-- luu gi vao store? [TODO] rebuild state hien tai bang cach nao? [TODO]
-- sau 2 nam, P001 hot-seller co bao nhieu event? [TODO: 800K/ngay x % hot x 730 ngay]
-- replay tu dau co cham khong? can co che gi de tang toc, nguong bao nhieu? [TODO]
-- =========================================================
-- TANG 5: DOMAIN ORDER -- chon pattern truy van
-- =========================================================
-- Yeu cau (nhom C): lich su don hang truy van NHANH, TACH khoi luong dat hang dang chay
-- Yeu cau "tach doc khoi ghi" goi y pattern nao? [ ] CQRS [ ] Event Sourcing [ ] CRUD
-- write side luu o dau, dang gi? [TODO] read side (view) gi, o store nao? [TODO]
-- Dong bo write -> read qua co che gi? [TODO] lag binh thuong / flash sale? [TODO]
-- Read-after-write: user POST /orders -> ngay GET /my-orders -> thay gi? [TODO]
-- cach xu ly stale ma khong pha vo tach biet read/write? [TODO]
-- =========================================================
-- TANG 6: LAMBDA hay KAPPA?
-- =========================================================
-- Tinh toan truoc khi chon:
-- OLShop: 800K don/ngay, moi order event ~ 200 bytes
-- Kafka storage (Kappa, giu de reprocess):
-- [TODO] events/ngay x [TODO] bytes x [TODO] ngay retention = [TODO] GB
-- x replication factor [TODO] = [TODO] GB tong
-- So voi S3 cold (batch layer trong Lambda):
-- ~$0.023/GB/thang vs Kafka hot ~$0.23/GB/thang -> gap bao nhieu?
-- Chon: [ ] Lambda [ ] Kappa
-- Ly do chinh: [TODO]
-- Neu Lambda: bao nhieu codebase maintain? [TODO] Rui ro gi? [TODO]
-- Neu Kappa: retention bao nhieu ngay? [TODO] Van de gi o quy mo OLShop vs Uber? [TODO]
-- =========================================================
-- TANG 7: DERIVED DATA & RECOMPUTABLE
-- =========================================================
-- 1. Redis inventory cache:
-- Recomputable? [TODO] Tu nguon nao? [TODO]
-- 2. Elasticsearch order search index:
-- Recomputable? [TODO] Tu nguon nao? [TODO]
-- 3. Monthly revenue report:
-- Recomputable? [TODO] Idempotent? [TODO]
-- 4. Event store (inventory history):
-- Day la SOURCE hay DERIVED? [TODO] Recomputable tu dau? [TODO]
-- 5. Order read model (CQRS):
-- Recomputable? [TODO] Tu nguon nao? [TODO]
-- 6. Anomaly alerts da gui ra ngoai:
-- Recomputable? [TODO] Tai sao khac cac loai tren? [TODO]
5. 💡 Gợi ý
Nếu chưa điền template ở mục 4, hãy quay lại làm trước. Gợi ý dưới chỉ đặt câu hỏi định hướng và công thức trần — không lộ kết luận, kiến trúc cụ thể, hay con số chốt. Toàn bộ ở §6.
5.1 Cơ chế phát changelog — crash ở đâu, mất gì?
Với dual-write, ứng dụng thực hiện hai thao tác: ghi DB rồi ghi Kafka (hoặc gọi Redis/ES trực tiếp). Hỏi: nếu crash xảy ra sau bước 1 nhưng trước bước 2, điều gì xảy ra với từng hệ thống? Và nếu thêm retry vào bước 2, nguy cơ mới nào xuất hiện khi bước 2 đã thành công nhưng ACK chưa về?
WAL-based CDC đọc transaction log của database — log này ghi trước khi commit và không thay đổi. Hỏi: điều gì xảy ra với event nếu ứng dụng crash trước khi đọc log? Outbox pattern khác WAL-CDC ở chỗ nào — infrastructure hay application code? Khi nào bạn không thể dùng WAL-CDC? (Kết luận và bảng so sánh ở §6.)
5.2 Event sourcing — khi nào replay quá chậm?
Mỗi lần cần current_stock bạn replay từ đầu toàn bộ event của aggregate đó. Với 800K đơn/ngày, một sản phẩm hot-seller chiếm vài phần trăm đơn hàng — sau 2 năm có bao nhiêu event? Replay bao nhiêu mili-giây mới chấp nhận được cho một API request đồng bộ (gợi ý: so với ngưỡng timeout HTTP thông thường)?
Snapshot lưu trạng thái tại một checkpoint. Hỏi: snap mỗi bao nhiêu event thì hợp lý — cân bằng giữa tần suất snapshot (chi phí ghi) và số event phải replay sau checkpoint (chi phí đọc)? (Ngưỡng cụ thể và ví dụ tính toán ở §6.)
5.3 Lambda hay Kappa — câu hỏi về chi phí storage
Công thức Kafka storage:
storage = events_per_day × bytes_per_event × retention_days × replication_factor
Trước khi chọn Kappa, hãy tự tính: với throughput của OLShop và retention đủ để reprocess, cần bao nhiêu GB? Kafka hot storage thường đắt hơn S3 cold storage bao nhiêu lần? Với quy mô OLShop (không phải vài chục nghìn events/giây như Uber), cán cân chi phí nghiêng về đâu?
Nếu chọn Lambda: đếm bao nhiêu codebase phải maintain cho cùng một phép tính doanh thu (batch Spark và stream Flink). Khi logic tính doanh thu thay đổi, bạn sửa bao nhiêu nơi? Rủi ro drift là gì? (Bảng so sánh định lượng ở §6.)
5.4 CQRS — read-after-write và lag
Sau khi user POST /orders thành công, họ ngay lập tức GET /my-orders. Read model cập nhật không đồng bộ qua Kafka — trong điều kiện bình thường độ trễ bao nhiêu? Trong flash sale?
Có những cách nào xử lý read-after-write mà không ép read model đồng bộ tức thời? (Gợi ý: đọc từ đâu cho request ngay sau write? Optimistic UI giải quyết vấn đề gì?) (Pattern cụ thể và bảng lag ở §6.)
5.5 Derived data — nguồn gốc quyết định recomputable
Câu hỏi cốt lõi cho mỗi loại: nếu mất hoàn toàn, bạn có thể tính lại không và từ dữ liệu nào? Redis inventory cache — từ đâu? Elasticsearch index — từ đâu? Monthly report — từ đâu?
Đặc biệt: nếu bạn chọn lưu inventory bằng một event store, nó là source of truth hay derived data? Mất nó rồi, bạn có tính lại được từ đâu không?
Còn anomaly alerts đã gửi ra bên ngoài — chúng khác gì Redis cache hay order index về khả năng tính lại? (Bảng phân tích đầy đủ ở §6.)
6. ✅ Lời giải tham khảo
6.1 Kiến trúc đầu-cuối
flowchart TB PG["PostgreSQL OLTP<br/>orders + products<br/>SOURCE OF RECORD"] DEB["Debezium CDC<br/>reads WAL log<br/>lag 100-500ms"] KC["Kafka orders-changelog<br/>key = order_id"] KI["Kafka inventory-changelog<br/>key = product_id"] SPA["Stream Processor A<br/>inventory update"] SPB["Stream Processor B<br/>anomaly detection 1-min window"] SPC["Stream Processor C<br/>ES index sync"] REDIS["Redis<br/>inventory cache (DERIVED)"] AA["Kafka anomaly-alerts (DERIVED)"] ES["Elasticsearch<br/>order search (DERIVED)"] BATCH["Spark batch job<br/>monthly report"] DWH["Data Warehouse<br/>monthly revenue (DERIVED)"] EVTST["Event Store<br/>inventory events<br/>append-only<br/>SOURCE for Inventory domain"] CQRS["Order read model<br/>Elasticsearch or PG replica<br/>(DERIVED)"] PG -->|"WAL"| DEB DEB --> KC DEB --> KI DEB -->|"inventory events"| EVTST KC --> SPA KC --> SPB KC --> SPC KC --> CQRS SPA -->|"idempotent SET"| REDIS SPB --> AA SPC -->|"upsert"| ES PG -->|"daily snapshot"| BATCH BATCH --> DWH
6.2 Cơ chế phát changelog — tại sao WAL-based CDC
Vấn đề dual-write khi crash:
-- DUAL-WRITE: 2 buoc rieng, khong atomic
app.save_order(order) -- 1. ghi PostgreSQL: COMMIT OK
-- !!! CRASH / Kafka producer timeout / network partition !!!
kafka.send("orders-changelog", ...) -- 2. khong chay duoc -> event mat
-- Ket qua: DB co don hang, Kafka khong co event
-- -> Redis + Elasticsearch khong biet -> lac hau am tham
-- Neu them retry: crash SAU KHI Kafka nhan nhung TRUOC KHI app nhan ACK
-- -> app retry -> Kafka co event 2 lan -> Redis cap nhat 2 lan
-- -> neu update la "stock -= qty": stock giam hai lan cho mot don hang -> loi am
WAL-based CDC (Debezium): đọc PostgreSQL WAL (Write-Ahead Log) — log bất biến, ghi trước commit, không bỏ sót event đã commit. Độ trễ 100–500ms sau commit. Không cần sửa schema hay application code.
Outbox Pattern (khi không có quyền dùng replication slot PostgreSQL): ghi event vào bảng outbox trong cùng một transaction với đơn hàng. Debezium đọc CDC từ bảng outbox. Kết quả tương đương, nhưng cần thêm bảng và ứng dụng phải tự sinh event.
-- OUTBOX: ghi event atomic voi don hang
BEGIN TRANSACTION
INSERT INTO orders (id, ...) VALUES (...)
INSERT INTO outbox (id, aggregate_id, event_type, payload)
VALUES (gen_id(), order_id, 'OrderPlaced',
'{"product_id": "P001", "qty": 5, "status": "PLACED"}')
COMMIT
-- Debezium doc bang outbox -> Kafka -> consumer
-- App code khong goi Kafka truc tiep -> khong co dual-write
| Cơ chế | Nguy cơ bất nhất | Yêu cầu hạ tầng | Lag |
|---|---|---|---|
| Dual-write | Cao (crash giữa 2 bước) | Không | dưới 10ms |
| WAL-based CDC | Không (log-based, atomicity do DB bảo đảm) | Replication slot PostgreSQL | 100–500ms |
| Outbox | Không (trong cùng transaction) | Bảng outbox + CDC | 100–500ms |
Khi nào không thể dùng WAL-CDC: không có quyền tạo replication slot (managed DB với hạn chế), hoặc cần kiểm soát schema event (Outbox cho phép thiết kế payload linh hoạt hơn WAL event).
6.3 Event sourcing cho domain Inventory — snapshot ngưỡng 1.000 event
Tính số event sau 2 năm:
OLShop 800.000 đơn/ngày. Sản phẩm hot-seller xuất hiện trong 5% đơn = 40.000 event/ngày:
-- Sau 2 nam: 40.000 event/ngay x 730 ngay = 29.200.000 event cho P001
-- Replay tu dau voi event store throughput ~50.000 event/giay:
-- 29.200.000 / 50.000 = 584 giay ~ 10 phut
-- -> khong chap nhan duoc cho API request dong bo (HTTP timeout 30s)
Snapshot ngưỡng thực hành: snapshot mỗi 1.000 event, hoặc khi replay vượt 100ms (ngưỡng khuyến nghị). Với snapshot:
-- Rebuild current_stock cho P001:
latest_snap <- snapshot_store.latest("P001")
-- (seq = 29.199.000, stock = 47, ts = 2028-07-08T09:00:00Z)
-- Chi replay tu snapshot den hien tai:
delta <- event_store.from("P001", from_seq = 29.199.000)
-- Uoc tinh: < 1.000 event (phat sinh tu 09:00 den now)
current_stock <- fold(delta, initial_state = latest_snap.stock)
-- Thoi gian: < 1.000 event x 0.02ms/event = 20ms -- chap nhan duoc
Khi nào Event Store là SOURCE, không phải DERIVED: Event Store của inventory nhận event từ PostgreSQL WAL qua CDC. Tuy nhiên, một khi event đã ghi vào Event Store, nó trở thành source of truth cho domain inventory — các read model (current_stock, reservation_history) là derived từ Event Store, không phải từ PostgreSQL OLTP nữa.
6.4 CQRS cho domain Order — xử lý read-after-write
Read model lag theo điều kiện:
| Điều kiện | Lag write → read |
|---|---|
| Bình thường | khoảng 50ms |
| Flash sale (100× tải) | 300ms trung bình, tail hơn 2 giây |
| Deployment | khoảng 5 giây |
| Consumer failure | Vài phút |
Kịch bản stale: user POST /orders → 201 Created → ngay lập tức GET /my-orders → read model chưa propagate → trả danh sách thiếu đơn vừa đặt → UX tệ, user bấm lại → double-submit.
Ba cách xử lý read-after-write:
-- Cach 1: route write-request den write model
-- Neu request co header X-Consistency: strong, router gui ve PostgreSQL primary
-- thay vi Elasticsearch read replica
-- Cach 2: optimistic UI
-- Client hien thi don hang ngay trong UI (truoc khi server confirm)
-- Background poll read model den khi dong bo
-- Cach 3: version token
-- POST /orders tra ve {"order_id": "...", "version": 42}
-- Client: GET /my-orders?min_version=42
-- Server: poll read model cho den khi read model version >= 42 roi tra ve
-- Timeout: neu qua 2s, tra ve fallback tu write model
6.5 Lambda hay Kappa — tính toán định lượng
Kafka storage (Kappa với retention 90 ngày):
-- OLShop throughput:
-- orders-changelog: 800.000 don/ngay x 200 bytes = 160 MB/ngay
-- inventory-changelog: 5M san pham, 10% thay doi/ngay = 500.000 x 200 bytes = 100 MB/ngay
-- Tong raw: ~260 MB/ngay
-- Retention 90 ngay de reprocess toan bo:
-- 260 MB x 90 = 23.4 GB raw
-- Replication factor 3: 23.4 x 3 = ~70 GB tong Kafka storage
-- Chi phi Kafka hot storage (Confluent Cloud Standard): ~$0.23/GB/thang
-- 70 GB x $0.23 = ~$16/thang -> nho, chap nhan duoc
-- So voi S3 cold (batch layer Lambda):
-- 23.4 GB x $0.023/GB = ~$0.54/thang -> re hon 30 lan
-- Nhung Lambda can maintain 2 codebase (Spark batch + Flink stream)
| Khía cạnh | Lambda | Kappa |
|---|---|---|
| Số codebase | 2 (Spark batch + Flink stream) | 1 (Flink stream + replay) |
| Rủi ro logic drift | Cao (batch/stream viết khác nhau) | Không |
| Storage | S3 cold (~$0.54/tháng cho OLShop) | Kafka hot (~$16/tháng) |
| Reprocess | Spark job từ S3 | Consumer version mới song song, rồi cutover |
| Phù hợp quy mô | Hệ lớn ~864 GB/ngày → ~78 TB Kafka/90 ngày = đắt gấp 10× (Uber còn lớn hơn — hàng PB) | OLShop (70 GB Kafka = rẻ) |
Kết luận cho OLShop: Kappa. Storage 70 GB không đáng kể so với chi phí maintain 2 codebase và rủi ro logic drift. Reprocess bằng cách chạy consumer version mới song song (đọc từ Kafka offset cũ) rồi cutover khi kết quả hội tụ.
Với một hệ lớn ~50.000 events/giây, giữ 90 ngày cần ~78 TB Kafka (chưa nhân replication; ×3 replicas ≈ 234 TB) — đắt hơn S3 khoảng 10 lần. Uber ở quy mô hàng triệu trips/ngày (hàng Petabyte) nên giới hạn retention chỉ vài ngày. OLShop không ở quy mô này.
6.6 Derived data — bảng recomputable
| Derived data | Recomputable? | Nguồn | Ghi chú |
|---|---|---|---|
| Redis inventory cache | Có | Kafka inventory-changelog + Event Store | Xóa Redis, replay lại từ Event Store |
| Elasticsearch order search | Có | Kafka orders-changelog | Re-index từ đầu (có thể mất vài giờ) |
| Monthly revenue report | Có | Snapshot OLTP hoặc Kafka changelog | Idempotent: chạy lại cho cùng kết quả |
| Event Store (inventory) | Một phần — đây là SOURCE cho Inventory domain | PostgreSQL WAL (input gốc) | Nếu mất Event Store, có thể rebuild từ WAL nếu WAL còn đủ retention |
| Order read model (CQRS) | Có | Kafka orders-changelog | Rebuild toàn bộ read model từ changelog |
| Anomaly alerts đã gửi | Không recomputable theo nghĩa hữu ích | — | Output đã ra ngoài hệ thống (side effect); có thể rerun pipeline nhưng không "gửi lại cảnh báo cũ" |
Nguyên tắc: Mọi derived data bên trong hệ thống đều recomputable nếu source of record còn đủ (PostgreSQL WAL hoặc Kafka changelog với đủ retention). Ngoại lệ: side effect đã ra bên ngoài — email, SMS, webhook, alert đã gửi — chúng là output irreversible, không phải state có thể tính lại.
7. 🎓 Mở rộng
Ba tình huống để suy nghĩ thêm sau khi đã hoàn thành lời giải tham khảo:
M1 — Schema evolution trong CDC: Team đổi tên cột user_id thành customer_id trong bảng orders. Debezium hiểu rename là xóa cột cũ rồi thêm cột mới — consumer đang expect user_id crash hoặc silent miss field. Bạn cần làm gì trước khi rename để các consumer downstream không bị ảnh hưởng? (Gợi ý: expand-contract migration — thêm customer_id trước, migrate dần, xóa user_id sau — kết hợp schema registry với compatibility mode backward.)
M2 — GDPR và event sourcing bất biến: Một người dùng yêu cầu xóa toàn bộ dữ liệu cá nhân (Right to Erasure). Event Store của inventory lưu tên người đặt hàng trong event payload. Event bất biến — không thể sửa hay xóa. Bạn xử lý bằng cách nào mà không phá vỡ tính bất biến? (Gợi ý: crypto-shredding — mã hóa PII trong event bằng key riêng từng user; xóa key khiến payload vô nghĩa mà không cần sửa event.)
M3 — Cutover từ dual-write sang CDC không downtime: OLShop đang chạy dual-write trong production. Bạn cần migration sang CDC mà không gián đoạn dịch vụ. Bước nào cần thực hiện trước (initial snapshot cho consumer, tạo topic, cấu hình Debezium connector)? Cần chạy song song dual-write và CDC bao lâu để đảm bảo không mất event trong quá trình cutover?
8. ✨ Điều bạn vừa làm được
-
Áp nguyên tắc input bất biến ở quy mô toàn hệ thống (Module 1): mọi derived view — Redis, Elasticsearch, báo cáo DWH, order read model — đều có thể tính lại từ source of record bất biến (PostgreSQL WAL, Kafka changelog). Khi Redis bị mất hoàn toàn, bạn replay inventory-changelog để rebuild. Đây là nguyên tắc cốt lõi Module 1 áp dụng không phải cho một pipeline mà cho toàn bộ kiến trúc dữ liệu.
-
Kết nối stream processing vào kiến trúc phái sinh (Module 2): CDC changelog chạy vào Flink stream processor để cập nhật Redis và Elasticsearch với latency 100–500ms, thay vì chờ batch chạy mỗi đêm. Windowing và watermark từ Module 2 áp trực tiếp cho anomaly detection (cửa sổ 1 phút, key theo account_id). Stream không thay thế batch — chúng giải quyết hai nhóm yêu cầu khác nhau (latency vs accuracy).
-
Phân biệt source of record và derived data ở cấp kiến trúc (Module 3 — bài 01 đến 04): PostgreSQL OLTP và Event Store của Inventory là source of truth — mọi thứ tính từ đó là derived. Quyết định WAL-CDC hay dual-write không phải chi tiết kỹ thuật nhỏ mà là ranh giới kiến trúc: ai là nguồn dữ liệu duy nhất, ai là view phái sinh. Nhầm ranh giới này dẫn đến bất nhất âm thầm như sự cố 12.000 sản phẩm của OLShop.
-
Lập luận định lượng Lambda vs Kappa theo quy mô (Module 3 — bài 04): không chọn theo trend mà tính 70 GB Kafka storage (rẻ hơn chi phí maintain 2 codebase cho OLShop) rồi kết luận Kappa thực tế hơn ở quy mô này. Tại quy mô rất lớn (hàng chục–trăm TB, và Uber tới hàng Petabyte), cán cân đổi chiều — Lambda rẻ hơn. Quyết định kiến trúc phụ thuộc vào số liệu thật của hệ thống bạn đang xây dựng.
-
Nhìn hệ thống theo chiều dọc xuyên cả track (Module 1 + 2 + 3): từ source of record → CDC → stream/batch → derived views là một chuỗi nhân quả liên tục. Batch giải quyết báo cáo chính xác (Module 1); stream giải quyết real-time (Module 2); CDC, Event Sourcing, CQRS, và Lambda/Kappa là bốn mắt xích để kết nối chúng mà không tạo ra dual-write hay coupling trực tiếp (Module 3). Input bất biến là sợi chỉ xuyên suốt — khi mọi thứ đều có thể tính lại từ nguồn, hệ thống có khả năng phục hồi từ bất kỳ lỗi nào.
Bài tiếp theo: Tổng kết module — cheat sheet & self-assessment
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên