Skip to main content

Event Sourcing — Lưu trữ sự kiện

"Event Sourcing ensures that all changes to application state are stored as a sequence of events. Not just the current state — the full story." — Martin Fowler


Tổng quan

Event Sourcing (ES) là một architectural pattern trong đó mọi thay đổi trạng thái của ứng dụng được lưu trữ như một chuỗi các sự kiện bất biến (immutable events), thay vì chỉ lưu trạng thái hiện tại. Để biết trạng thái hiện tại, ta replay tất cả các sự kiện từ đầu.

Ý tưởng này đã tồn tại từ lâu trong lĩnh vực tài chính (audit log, ledger) và được phổ biến trong software engineering bởi:

  • Martin Fowler — Bài viết kinh điển (2005), tổng quan và phân tích sâu
  • Greg Young — Kết hợp Event Sourcing với CQRS, phổ biến trong cộng đồng .NET
  • Eric Evans — Domain-Driven Design cung cấp nền tảng lý thuyết
  • Udi Dahan — Áp dụng ES trong hệ thống enterprise SOA
  • Chris Richardson — Microservices patterns, saga pattern với ES

So sánh với CRUD truyền thống

# CRUD — chỉ lưu trạng thái hiện tại
db.orders.update({"id": 123, "status": "SHIPPED", "total": 500000})

# Event Sourcing — lưu mọi sự kiện
event_store.append(OrderShipped(order_id=123, timestamp=...))
event_store.append(OrderTotalAdjusted(order_id=123, amount=500000, ...))
Khía cạnhCRUDEvent Sourcing
Lưu trữTrạng thái hiện tạiChuỗi sự kiện bất biến
Lịch sửKhông (hoặc phải thêm audit log)Có sẵn, đầy đủ
DebugKhó biết ai đã thay đổi gìReplay events để debug
Temporal queryKhông hỗ trợCó thể query bất kỳ thời điểm nào
Phức tạpThấpCao

Bài toán

Audit trail và compliance

Trong các hệ thống tài chính, y tế, hoặc pháp lý, việc có một audit trail đầy đủ là bắt buộc. Với CRUD, khi ai đó sửa một record, thông tin cũ sẽ bị mất. Bạn phải xây dựng thêm audit log riêng — và audit log này thường không đồng bộ với dữ liệu chính.

Ví dụ: Một nhân viên ngân hàng sửa số dư tài khoản của khách hàng. Với CRUD, bạn chỉ thấy số dư mới, không biết ai đã sửa, sửa từ đâu, tại sao. Với Event Sourcing, mọi thao tác đều là event bất biến: AdminForcedBalanceChanged(admin_id, old_balance, new_balance, reason, timestamp).

Temporal query và business intelligence

"Doanh thu tháng 6 năm ngoái là bao nhiêu?" — Với CRUD, nếu bạn không lưu lịch sử, bạn không thể trả lời. Bạn cần một data warehouse riêng, ETL process, và đủ thứ phức tạp.

Event Sourcing cho phép bạn temporal query:

  • Trạng thái hệ thống tại bất kỳ thời điểm nào trong quá khứ
  • Replay events để tái tạo dữ liệu cho báo cáo
  • Time travel debugging

Debugging và incident response

Khi có bug, bạn cần biết chính xác điều gì đã xảy ra. Với CRUD, bạn chỉ thấy kết quả cuối cùng. Bạn phải guess. Với ES, bạn replay events trong môi trường dev, tái tạo chính xác trạng thái lúc bug xảy ra, và debug từng bước.

Complex event processing

Hệ thống cần phản ứng với các pattern phức tạp:

  • "Phát hiện gian lận khi user đăng nhập từ 3 quốc gia khác nhau trong 1 giờ"
  • "Tự động hủy đơn hàng nếu thanh toán không hoàn tất sau 24h"
  • "Gửi email chúc mừng khi user đạt 100 đơn hàng"

Với ES, bạn có event stream, dễ dàng áp dụng Complex Event Processing (CEP) để phát hiện pattern.


Nguyên lý thiết kế

1. Event là bất biến (Immutable)

Một event, khi đã được lưu, không bao giờ được sửa hoặc xóa. Nếu phát hiện sai, bạn tạo event mới để correct:

# Sai: sửa event cũ
event.amount = 100 # ❌ Không bao giờ

# Đúng: tạo event mới
event_store.append(OrderCorrected(order_id, reason="Sai số tiền", correction=...))

2. Current state là derived data

Trạng thái hiện tại được tính bằng cách replay tất cả events từ đầu. Đây là projection:

def get_account_balance(account_id):
balance = 0
for event in event_store.get_events(account_id):
if isinstance(event, MoneyDeposited):
balance += event.amount
elif isinstance(event, MoneyWithdrawn):
balance -= event.amount
return balance

3. Event là sự thật (Event as Truth)

Event store là single source of truth. Mọi thứ khác (read models, caches, indexes) đều là projection và có thể được rebuild từ events.

4. Snapshot cho performance

Khi number of events quá lớn (hàng triệu), replay từ đầu có thể chậm. Snapshot lưu trạng thái tại một thời điểm để làm điểm khởi đầu:

account = snapshot_store.get(account_id) # State tại version 1000
for event in event_store.get_events(account_id, from_version=1001):
account.apply(event)

Cấu trúc chi tiết

Các thành phần chính

┌─────────────────────────────────────────────────────────────────────┐
│ Event Sourcing System │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Event Store │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Events: [E1] → [E2] → [E3] → [E4] → [E5] → ... │ │ │
│ │ │ Immutable, Append-only, Chronological │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ Projection 1 │ │ Projection │ │ Projection 3 │ │
│ │ (Current State) │ │ (Read │ │ (Analytics) │ │
│ │ │ │ Model) │ │ │ │
│ │ Order Aggregate │ │ OrderDTO │ │ Revenue/Month│ │
│ └──────────────────┘ └────────────┘ └──────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Commands │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Validate │→│ Business │→│ Append │ │ │
│ │ │ │ │ Logic │ │ Event │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Event Bus │ │
│ │ Khi event được lưu, nó được publish cho các projector │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

Event Store

  • Append-only: Events chỉ được thêm, không sửa/xóa
  • Chronological: Events có thứ tự thời gian
  • Immutable: Một event đã lưu không thay đổi
  • Replayable: Có thể replay từ đầu

Aggregate

  • Là đơn vị transaction (theo DDD)
  • Nhận command → validate → sinh event → append
  • Có version để optimistic concurrency

Projection

  • Đọc events từ event store
  • Tính toán và update read model
  • Có thể rebuild từ đầu
  • Idempotent (chạy lại không gây hại)

Sơ đồ kiến trúc

┌─────────────┐
│ Command │
│ (External) │
└──────┬──────┘


┌─────────────────────┐
│ Command Handler │
│ • Load aggregate │
│ • Validate │
│ • Execute │
│ • Generate events │
└──────────┬──────────┘

┌──────────▼──────────┐
│ Event Store │
│ ┌────────────────┐ │
│ │ Event 1 │ │──────┐
│ │ Event 2 │ │ │
│ │ Event 3 │ │ │
│ │ ... │ │ │
│ └────────────────┘ │ │
└─────────────────────┘ │

┌─────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Projector │ │ Projector │ │ Projector │
│ (Current │ │ (Read DB) │ │ (Analytics) │
│ State) │ │ │ │ │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Snapshot │ │ PostgreSQL │ │ Elastic │
│ Store │ │ (Read Model)│ │ Search │
└──────────────┘ └──────────────┘ └──────────────┘


┌──────────────┐
│ API/Query │
│ (FastAPI) │
└──────────────┘

Ví dụ code hoàn chỉnh

Xây dựng hệ thống quản lý giỏ hàng thương mại điện tử với Event Sourcing.

Cấu trúc project

cart_es/
├── domain/
│ ├── __init__.py
│ ├── events.py
│ ├── cart.py
│ └── product.py
├── infrastructure/
│ ├── __init__.py
│ ├── event_store.py
│ ├── snapshot_store.py
│ └── projections.py
├── application/
│ ├── __init__.py
│ ├── commands.py
│ ├── queries.py
│ └── handlers.py
├── tests/
│ ├── __init__.py
│ ├── test_cart.py
│ ├── test_event_store.py
│ └── test_projections.py
└── main.py

File: domain/events.py

from __future__ import annotations

from abc import ABC
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from enum import Enum, auto
from typing import Optional
from uuid import UUID, uuid4


class EventVersion(Enum):
"""Phiên bản event schema (cho migration)."""
V1 = auto()


@dataclass
class DomainEvent(ABC):
"""Base class cho mọi domain event (bất biến)."""
event_id: UUID = field(default_factory=uuid4)
aggregate_id: UUID = field(default_factory=uuid4)
version: int = 1
timestamp: datetime = field(default_factory=datetime.utcnow)

def __post_init__(self) -> None:
"""Đảm bảo tính bất biến — raise error nếu ai đó cố sửa."""
pass


@dataclass
class CartCreated(DomainEvent):
"""Giỏ hàng được tạo."""
customer_id: UUID
created_at: datetime = field(default_factory=datetime.utcnow)


@dataclass
class ItemAddedToCart(DomainEvent):
"""Sản phẩm được thêm vào giỏ."""
product_id: UUID
product_name: str
price: Decimal
quantity: int
currency: str = "VND"


@dataclass
class ItemQuantityChanged(DomainEvent):
"""Số lượng sản phẩm trong giỏ thay đổi."""
product_id: UUID
old_quantity: int
new_quantity: int


@dataclass
class ItemRemovedFromCart(DomainEvent):
"""Sản phẩm bị xóa khỏi giỏ."""
product_id: UUID
product_name: str
removed_quantity: int


@dataclass
class DiscountApplied(DomainEvent):
"""Mã giảm giá được áp dụng."""
coupon_code: str
discount_amount: Decimal
discount_type: str # PERCENTAGE, FIXED


@dataclass
class CartCleared(DomainEvent):
"""Giỏ hàng bị xóa sạch."""
reason: str = ""


@dataclass
class CartCheckedOut(DomainEvent):
"""Giỏ hàng được thanh toán."""
total_amount: Decimal
payment_method: str
shipping_address: str

File: domain/cart.py

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from typing import Optional
from uuid import UUID, uuid4

from domain.events import *


class CartError(Exception):
"""Base exception cho cart domain."""
pass


class CartNotFoundError(CartError):
pass


class CartEmptyError(CartError):
pass


class ProductNotInCartError(CartError):
pass


class InvalidQuantityError(CartError):
pass


@dataclass
class CartItem:
"""Item trong giỏ hàng (value object cho current state)."""
product_id: UUID
product_name: str
price: Decimal
quantity: int
currency: str = "VND"

@property
def subtotal(self) -> Decimal:
return self.price * Decimal(str(self.quantity))


@dataclass
class Cart:
"""Aggregate: Giỏ hàng — trạng thái hiện tại được tính từ events."""
id: UUID
customer_id: UUID
items: dict[UUID, CartItem]
applied_discount: Optional[dict] = None
is_checked_out: bool = False
created_at: Optional[datetime] = None
version: int = 0

@classmethod
def create(cls, customer_id: UUID) -> tuple[Cart, list[DomainEvent]]:
"""Factory method tạo giỏ hàng mới."""
cart_id = uuid4()
event = CartCreated(
aggregate_id=cart_id,
customer_id=customer_id,
)
cart = cls._from_events([event])
return cart, [event]

@classmethod
def _from_events(cls, events: list[DomainEvent]) -> Cart:
"""Tái tạo Cart từ danh sách events."""
cart = cls(id=uuid4(), customer_id=uuid4(), items={})
for event in events:
cart._apply(event)
return cart

def apply(self, event: DomainEvent) -> None:
"""Apply một event vào aggregate."""
self._apply(event)
self.version += 1

def _apply(self, event: DomainEvent) -> None:
"""Internal apply — cập nhật trạng thái dựa trên event."""
if isinstance(event, CartCreated):
self.id = event.aggregate_id
self.customer_id = event.customer_id
self.created_at = event.timestamp
elif isinstance(event, ItemAddedToCart):
item = CartItem(
product_id=event.product_id,
product_name=event.product_name,
price=event.price,
quantity=event.quantity,
currency=event.currency,
)
if event.product_id in self.items:
existing = self.items[event.product_id]
existing.quantity += event.quantity
else:
self.items[event.product_id] = item
elif isinstance(event, ItemQuantityChanged):
if event.product_id in self.items:
self.items[event.product_id].quantity = event.new_quantity
elif isinstance(event, ItemRemovedFromCart):
self.items.pop(event.product_id, None)
elif isinstance(event, DiscountApplied):
self.applied_discount = {
"code": event.coupon_code,
"amount": event.discount_amount,
"type": event.discount_type,
}
elif isinstance(event, CartCleared):
self.items.clear()
self.applied_discount = None
elif isinstance(event, CartCheckedOut):
self.is_checked_out = True

def add_item(
self, product_id: UUID, product_name: str,
price: Decimal, quantity: int, currency: str = "VND"
) -> list[DomainEvent]:
"""Thêm item vào giỏ — sinh event."""
if quantity <= 0:
raise InvalidQuantityError("Số lượng phải lớn hơn 0")
if self.is_checked_out:
raise CartError("Giỏ hàng đã thanh toán")

event = ItemAddedToCart(
aggregate_id=self.id,
product_id=product_id,
product_name=product_name,
price=price,
quantity=quantity,
currency=currency,
)
return [event]

def change_quantity(self, product_id: UUID, new_quantity: int) -> list[DomainEvent]:
"""Thay đổi số lượng — sinh event."""
if product_id not in self.items:
raise ProductNotInCartError("Sản phẩm không có trong giỏ")
if new_quantity <= 0:
raise InvalidQuantityError("Số lượng phải lớn hơn 0")
if self.is_checked_out:
raise CartError("Giỏ hàng đã thanh toán")

old_qty = self.items[product_id].quantity
if old_qty == new_quantity:
return []

event = ItemQuantityChanged(
aggregate_id=self.id,
product_id=product_id,
old_quantity=old_qty,
new_quantity=new_quantity,
)
return [event]

def remove_item(self, product_id: UUID) -> list[DomainEvent]:
"""Xóa item — sinh event."""
if product_id not in self.items:
raise ProductNotInCartError("Sản phẩm không có trong giỏ")
if self.is_checked_out:
raise CartError("Giỏ hàng đã thanh toán")

item = self.items[product_id]
event = ItemRemovedFromCart(
aggregate_id=self.id,
product_id=product_id,
product_name=item.product_name,
removed_quantity=item.quantity,
)
return [event]

def apply_discount(self, code: str, amount: Decimal, discount_type: str) -> list[DomainEvent]:
"""Áp dụng mã giảm giá — sinh event."""
if self.is_checked_out:
raise CartError("Giỏ hàng đã thanh toán")

event = DiscountApplied(
aggregate_id=self.id,
coupon_code=code,
discount_amount=amount,
discount_type=discount_type,
)
return [event]

def clear(self, reason: str = "") -> list[DomainEvent]:
"""Xóa giỏ hàng — sinh event."""
if self.is_checked_out:
raise CartError("Giỏ hàng đã thanh toán")

event = CartCleared(
aggregate_id=self.id,
reason=reason,
)
return [event]

def checkout(self, payment_method: str, shipping_address: str) -> list[DomainEvent]:
"""Thanh toán giỏ hàng — sinh event."""
if not self.items:
raise CartEmptyError("Giỏ hàng trống")
if self.is_checked_out:
raise CartError("Giỏ hàng đã thanh toán")

total = self.calculate_total()
event = CartCheckedOut(
aggregate_id=self.id,
total_amount=total,
payment_method=payment_method,
shipping_address=shipping_address,
)
return [event]

def calculate_total(self) -> Decimal:
"""Tính tổng tiền."""
total = sum(item.subtotal for item in self.items.values())
if self.applied_discount:
if self.applied_discount["type"] == "PERCENTAGE":
discount = total * self.applied_discount["amount"] / Decimal("100")
total -= discount
elif self.applied_discount["type"] == "FIXED":
total -= self.applied_discount["amount"]
return max(total, Decimal("0"))

@property
def item_count(self) -> int:
return sum(item.quantity for item in self.items.values())

@property
def unique_items(self) -> int:
return len(self.items)

File: domain/product.py

from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
from uuid import UUID


@dataclass
class ProductInfo:
"""Thông tin sản phẩm (value object)."""
product_id: UUID
name: str
price: Decimal
currency: str = "VND"
stock: int = 0
is_available: bool = True

File: infrastructure/event_store.py

from __future__ import annotations

import json
from datetime import datetime
from pathlib import Path
from typing import Optional
from uuid import UUID

from domain.events import DomainEvent


class EventStore:
"""Event Store — append-only, immutable event storage.

Trong production, dùng PostgreSQL, EventStoreDB, hoặc Kafka.
Ở đây dùng in-memory + file-based persistence cho demo.
"""

def __init__(self, storage_path: Optional[Path] = None):
self._storage_path = storage_path or Path("events.jsonl")
self._events_by_aggregate: dict[UUID, list[DomainEvent]] = {}
self._all_events: list[DomainEvent] = []
self._load_from_disk()

def append(self, aggregate_id: UUID, events: list[DomainEvent], expected_version: int) -> None:
"""Append events với optimistic concurrency control."""
if aggregate_id not in self._events_by_aggregate:
self._events_by_aggregate[aggregate_id] = []
current_version = 0
else:
current_version = len(self._events_by_aggregate[aggregate_id])

if current_version != expected_version:
raise ConcurrencyError(
f"Conflict: expected {expected_version}, current {current_version}"
)

for event in events:
event.event_id = UUID(event.event_id) if isinstance(event.event_id, str) else event.event_id
event.aggregate_id = aggregate_id
self._events_by_aggregate[aggregate_id].append(event)
self._all_events.append(event)
self._save_to_disk(event)

def get_events(self, aggregate_id: UUID) -> list[DomainEvent]:
"""Lấy tất cả events của một aggregate."""
return list(self._events_by_aggregate.get(aggregate_id, []))

def get_events_since(self, aggregate_id: UUID, from_version: int) -> list[DomainEvent]:
"""Lấy events từ một version."""
events = self._events_by_aggregate.get(aggregate_id, [])
return events[from_version:]

def get_all_events(self) -> list[DomainEvent]:
"""Lấy tất cả events."""
return list(self._all_events)

def get_all_aggregate_ids(self) -> list[UUID]:
"""Lấy danh sách tất cả aggregate IDs."""
return list(self._events_by_aggregate.keys())

def count_events(self, aggregate_id: UUID) -> int:
"""Đếm số events của một aggregate."""
return len(self._events_by_aggregate.get(aggregate_id, []))

def _save_to_disk(self, event: DomainEvent) -> None:
"""Persist event xuống disk (JSONL format)."""
try:
with open(self._storage_path, "a", encoding="utf-8") as f:
data = {
"event_type": type(event).__name__,
"aggregate_id": str(event.aggregate_id),
"event_id": str(event.event_id),
"timestamp": event.timestamp.isoformat() if isinstance(event.timestamp, datetime) else event.timestamp,
"data": self._serialize_event(event),
}
f.write(json.dumps(data, ensure_ascii=False) + "\n")
except IOError:
pass # Trong demo, ignore disk errors

def _serialize_event(self, event: DomainEvent) -> dict:
"""Serialize event data."""
result = {}
for attr in vars(event):
if attr.startswith("_"):
continue
value = getattr(event, attr)
if isinstance(value, UUID):
result[attr] = str(value)
elif isinstance(value, datetime):
result[attr] = value.isoformat()
elif isinstance(value, Decimal):
result[attr] = str(value)
else:
result[attr] = value
return result

def _load_from_disk(self) -> None:
"""Load events từ disk vào memory."""
if not self._storage_path.exists():
return
# Trong thực tế, cần deserialize events
pass


class ConcurrencyError(Exception):
"""Xung đột optimistic locking."""
pass

File: infrastructure/snapshot_store.py

from __future__ import annotations

import pickle
from pathlib import Path
from typing import Optional
from uuid import UUID

from domain.cart import Cart


class SnapshotStore:
"""Lưu snapshot của aggregate để tăng tốc replay.

Thay vì replay từ event đầu tiên, ta chỉ replay từ snapshot gần nhất.
"""

def __init__(self, storage_path: Optional[Path] = None):
self._storage_path = storage_path or Path("snapshots")
self._storage_path.mkdir(exist_ok=True)
self._snapshots: dict[UUID, tuple[Cart, int]] = {}

def save(self, aggregate_id: UUID, cart: Cart, version: int) -> None:
"""Lưu snapshot."""
self._snapshots[aggregate_id] = (cart, version)
# File-based persistence (pickle for demo)
snapshot_file = self._storage_path / f"{aggregate_id}.snap"
try:
with open(snapshot_file, "wb") as f:
pickle.dump((cart, version), f)
except IOError:
pass

def load(self, aggregate_id: UUID) -> Optional[tuple[Cart, int]]:
"""Load snapshot gần nhất."""
# Check memory first
if aggregate_id in self._snapshots:
return self._snapshots[aggregate_id]

# Check disk
snapshot_file = self._storage_path / f"{aggregate_id}.snap"
if snapshot_file.exists():
try:
with open(snapshot_file, "rb") as f:
snapshot = pickle.load(f)
self._snapshots[aggregate_id] = snapshot
return snapshot
except (pickle.PickleError, IOError):
pass
return None

def clear(self, aggregate_id: Optional[UUID] = None) -> None:
"""Xóa snapshot."""
if aggregate_id:
self._snapshots.pop(aggregate_id, None)
snapshot_file = self._storage_path / f"{aggregate_id}.snap"
snapshot_file.unlink(missing_ok=True)
else:
self._snapshots.clear()
for f in self._storage_path.glob("*.snap"):
f.unlink(missing_ok=True)

File: infrastructure/projections.py

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from typing import Any, Optional
from uuid import UUID

from domain.events import *
from domain.cart import Cart, CartItem


@dataclass
class CartProjection:
"""Read model: thông tin giỏ hàng cho hiển thị."""
cart_id: UUID
customer_id: UUID
items: list[dict]
total: Decimal
item_count: int
unique_items: int
applied_discount: Optional[dict]
is_checked_out: bool
created_at: Optional[str]
last_updated: Optional[str]


@dataclass
class CustomerCartSummary:
"""Read model: tổng quan giỏ hàng của khách hàng."""
customer_id: UUID
active_carts: int
total_items_in_carts: int
total_value: Decimal
last_activity: Optional[str]


class CartProjector:
"""Projector: cập nhật read models từ events."""

def __init__(self):
self._cart_read_models: dict[UUID, CartProjection] = {}
self._customer_summaries: dict[UUID, CustomerCartSummary] = {}

def project(self, event: DomainEvent) -> None:
"""Xử lý event và cập nhật projection."""
handler = self._get_handler(event)
if handler:
handler(event)

def _get_handler(self, event: DomainEvent):
handlers = {
CartCreated: self._on_cart_created,
ItemAddedToCart: self._on_item_added,
ItemQuantityChanged: self._on_quantity_changed,
ItemRemovedFromCart: self._on_item_removed,
DiscountApplied: self._on_discount_applied,
CartCleared: self._on_cart_cleared,
CartCheckedOut: self._on_cart_checked_out,
}
return handlers.get(type(event))

def _on_cart_created(self, event: CartCreated) -> None:
projection = CartProjection(
cart_id=event.aggregate_id,
customer_id=event.customer_id,
items=[],
total=Decimal("0"),
item_count=0,
unique_items=0,
applied_discount=None,
is_checked_out=False,
created_at=event.timestamp.isoformat(),
last_updated=event.timestamp.isoformat(),
)
self._cart_read_models[event.aggregate_id] = projection
self._update_customer_summary(event.customer_id)

def _on_item_added(self, event: ItemAddedToCart) -> None:
projection = self._cart_read_models.get(event.aggregate_id)
if not projection:
return

# Tìm item hiện tại
existing = None
for item in projection.items:
if item["product_id"] == str(event.product_id):
existing = item
break

if existing:
existing["quantity"] += event.quantity
existing["subtotal"] = float(
Decimal(str(existing["price"])) * Decimal(str(existing["quantity"]))
)
else:
projection.items.append({
"product_id": str(event.product_id),
"product_name": event.product_name,
"price": float(event.price),
"quantity": event.quantity,
"currency": event.currency,
"subtotal": float(event.price * Decimal(str(event.quantity))),
})

projection.total = sum(
Decimal(str(item["subtotal"])) for item in projection.items
)
projection.item_count = sum(item["quantity"] for item in projection.items)
projection.unique_items = len(projection.items)
projection.last_updated = event.timestamp.isoformat()

self._update_customer_summary(projection.customer_id)

def _on_quantity_changed(self, event: ItemQuantityChanged) -> None:
projection = self._cart_read_models.get(event.aggregate_id)
if not projection:
return

for item in projection.items:
if item["product_id"] == str(event.product_id):
item["quantity"] = event.new_quantity
item["subtotal"] = float(
Decimal(str(item["price"])) * Decimal(str(event.new_quantity))
)
break

projection.total = sum(
Decimal(str(item["subtotal"])) for item in projection.items
)
projection.item_count = sum(item["quantity"] for item in projection.items)
projection.last_updated = event.timestamp.isoformat()

def _on_item_removed(self, event: ItemRemovedFromCart) -> None:
projection = self._cart_read_models.get(event.aggregate_id)
if not projection:
return

projection.items = [
item for item in projection.items
if item["product_id"] != str(event.product_id)
]
projection.total = sum(
Decimal(str(item["subtotal"])) for item in projection.items
)
projection.item_count = sum(item["quantity"] for item in projection.items)
projection.unique_items = len(projection.items)
projection.last_updated = event.timestamp.isoformat()

self._update_customer_summary(projection.customer_id)

def _on_discount_applied(self, event: DiscountApplied) -> None:
projection = self._cart_read_models.get(event.aggregate_id)
if not projection:
return

projection.applied_discount = {
"code": event.coupon_code,
"amount": float(event.discount_amount),
"type": event.discount_type,
}
projection.last_updated = event.timestamp.isoformat()

def _on_cart_cleared(self, event: CartCleared) -> None:
projection = self._cart_read_models.get(event.aggregate_id)
if not projection:
return

projection.items = []
projection.total = Decimal("0")
projection.item_count = 0
projection.unique_items = 0
projection.applied_discount = None
projection.last_updated = event.timestamp.isoformat()

self._update_customer_summary(projection.customer_id)

def _on_cart_checked_out(self, event: CartCheckedOut) -> None:
projection = self._cart_read_models.get(event.aggregate_id)
if not projection:
return

projection.is_checked_out = True
projection.last_updated = event.timestamp.isoformat()

self._update_customer_summary(projection.customer_id)

def _update_customer_summary(self, customer_id: UUID) -> None:
"""Cập nhật summary cho khách hàng."""
active_carts = [
c for c in self._cart_read_models.values()
if c.customer_id == customer_id and not c.is_checked_out
]
total_items = sum(c.item_count for c in active_carts)
total_value = sum(c.total if isinstance(c.total, Decimal) else Decimal(str(c.total)) for c in active_carts)
last_activity = max(
(c.last_updated for c in active_carts if c.last_updated),
default=None,
)

self._customer_summaries[customer_id] = CustomerCartSummary(
customer_id=customer_id,
active_carts=len(active_carts),
total_items_in_carts=total_items,
total_value=total_value,
last_activity=last_activity,
)

def get_cart_projection(self, cart_id: UUID) -> Optional[CartProjection]:
"""Lấy read model của giỏ hàng."""
return self._cart_read_models.get(cart_id)

def get_customer_summary(self, customer_id: UUID) -> Optional[CustomerCartSummary]:
"""Lấy summary của khách hàng."""
return self._customer_summaries.get(customer_id)

def rebuild_from_events(self, all_events: list[DomainEvent]) -> None:
"""Rebuild tất cả projections từ events (idempotent)."""
self._cart_read_models.clear()
self._customer_summaries.clear()
for event in all_events:
self.project(event)

File: application/commands.py

from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal
from typing import Optional
from uuid import UUID, uuid4


@dataclass
class CreateCartCommand:
"""Command: Tạo giỏ hàng mới."""
customer_id: UUID


@dataclass
class AddItemCommand:
"""Command: Thêm sản phẩm vào giỏ."""
cart_id: UUID
product_id: UUID
product_name: str
price: Decimal
quantity: int
currency: str = "VND"
idempotency_key: str = field(default_factory=lambda: str(uuid4()))


@dataclass
class ChangeQuantityCommand:
"""Command: Thay đổi số lượng."""
cart_id: UUID
product_id: UUID
new_quantity: int


@dataclass
class RemoveItemCommand:
"""Command: Xóa sản phẩm khỏi giỏ."""
cart_id: UUID
product_id: UUID


@dataclass
class ApplyDiscountCommand:
"""Command: Áp dụng mã giảm giá."""
cart_id: UUID
coupon_code: str
amount: Decimal
discount_type: str # PERCENTAGE, FIXED


@dataclass
class ClearCartCommand:
"""Command: Xóa giỏ hàng."""
cart_id: UUID
reason: str = ""


@dataclass
class CheckoutCommand:
"""Command: Thanh toán giỏ hàng."""
cart_id: UUID
payment_method: str
shipping_address: str

File: application/handlers.py

from __future__ import annotations

from typing import Any, Optional
from uuid import UUID

from domain.cart import Cart
from domain.events import DomainEvent
from infrastructure.event_store import EventStore, ConcurrencyError
from infrastructure.snapshot_store import SnapshotStore
from infrastructure.projections import CartProjector, CartProjection


class CartCommandHandler:
"""Xử lý commands cho giỏ hàng."""

SNAPSHOT_FREQUENCY = 10 # Lưu snapshot mỗi 10 events

def __init__(
self,
event_store: EventStore,
snapshot_store: Optional[SnapshotStore] = None,
projector: Optional[CartProjector] = None,
):
self._event_store = event_store
self._snapshot_store = snapshot_store or SnapshotStore()
self._projector = projector

def handle_create_cart(self, cmd) -> dict:
"""Xử lý tạo giỏ hàng."""
cart, events = Cart.create(cmd.customer_id)
self._event_store.append(cart.id, events, 0)
cart.apply(events[-1]) # Cập nhật local state

if self._projector:
for event in events:
self._projector.project(event)

self._maybe_save_snapshot(cart.id, cart)

return {"cart_id": str(cart.id), "events_appended": len(events)}

def handle_add_item(self, cmd) -> dict:
"""Xử lý thêm item."""
cart = self._load_cart(cmd.cart_id)
events = cart.add_item(
product_id=cmd.product_id,
product_name=cmd.product_name,
price=cmd.price,
quantity=cmd.quantity,
currency=cmd.currency,
)
expected_version = self._event_store.count_events(cart.id)
self._event_store.append(cart.id, events, expected_version)

for event in events:
cart.apply(event)

if self._projector:
for event in events:
self._projector.project(event)

self._maybe_save_snapshot(cart.id, cart)

return {
"cart_id": str(cart.id),
"item_count": cart.item_count,
"total": float(cart.calculate_total()),
"events_appended": len(events),
}

def handle_change_quantity(self, cmd) -> dict:
"""Xử lý thay đổi số lượng."""
cart = self._load_cart(cmd.cart_id)
events = cart.change_quantity(cmd.product_id, cmd.new_quantity)
if events:
expected_version = self._event_store.count_events(cart.id)
self._event_store.append(cart.id, events, expected_version)

for event in events:
cart.apply(event)

if self._projector:
for event in events:
self._projector.project(event)

self._maybe_save_snapshot(cart.id, cart)

return {
"cart_id": str(cart.id),
"item_count": cart.item_count,
"total": float(cart.calculate_total()),
}

def handle_remove_item(self, cmd) -> dict:
"""Xử lý xóa item."""
cart = self._load_cart(cmd.cart_id)
events = cart.remove_item(cmd.product_id)
expected_version = self._event_store.count_events(cart.id)
self._event_store.append(cart.id, events, expected_version)

for event in events:
cart.apply(event)

if self._projector:
for event in events:
self._projector.project(event)

self._maybe_save_snapshot(cart.id, cart)

return {
"cart_id": str(cart.id),
"item_count": cart.item_count,
"total": float(cart.calculate_total()),
}

def handle_apply_discount(self, cmd) -> dict:
"""Xử lý áp dụng mã giảm giá."""
cart = self._load_cart(cmd.cart_id)
events = cart.apply_discount(cmd.coupon_code, cmd.amount, cmd.discount_type)
expected_version = self._event_store.count_events(cart.id)
self._event_store.append(cart.id, events, expected_version)

for event in events:
cart.apply(event)

if self._projector:
for event in events:
self._projector.project(event)

return {
"cart_id": str(cart.id),
"total": float(cart.calculate_total()),
"discount": float(cmd.amount),
}

def handle_clear_cart(self, cmd) -> dict:
"""Xử lý xóa giỏ hàng."""
cart = self._load_cart(cmd.cart_id)
events = cart.clear(cmd.reason)
expected_version = self._event_store.count_events(cart.id)
self._event_store.append(cart.id, events, expected_version)

for event in events:
cart.apply(event)

if self._projector:
for event in events:
self._projector.project(event)

return {
"cart_id": str(cart.id),
"cleared": True,
}

def handle_checkout(self, cmd) -> dict:
"""Xử lý thanh toán."""
cart = self._load_cart(cmd.cart_id)
events = cart.checkout(cmd.payment_method, cmd.shipping_address)
expected_version = self._event_store.count_events(cart.id)
self._event_store.append(cart.id, events, expected_version)

for event in events:
cart.apply(event)

if self._projector:
for event in events:
self._projector.project(event)

return {
"cart_id": str(cart.id),
"total": float(cart.calculate_total()),
"item_count": cart.item_count,
}

def _load_cart(self, cart_id: UUID) -> Cart:
"""Load cart với snapshot optimization."""
# Thử load từ snapshot
snapshot = self._snapshot_store.load(cart_id)
if snapshot:
cart, snapshot_version = snapshot
start_version = snapshot_version
else:
cart = Cart(id=cart_id, customer_id=UUID(int=0), items={})
start_version = 0

# Replay các events từ snapshot đến hiện tại
events = self._event_store.get_events_since(cart_id, start_version)
for event in events:
cart.apply(event)

return cart

def _maybe_save_snapshot(self, cart_id: UUID, cart: Cart) -> None:
"""Lưu snapshot định kỳ."""
event_count = self._event_store.count_events(cart_id)
if event_count > 0 and event_count % self.SNAPSHOT_FREQUENCY == 0:
self._snapshot_store.save(cart_id, cart, event_count)

File: main.py

#!/usr/bin/env python3
"""
Event Sourcing E-Commerce Cart — Ví dụ hoàn chỉnh.
"""

from __future__ import annotations

from decimal import Decimal
from uuid import UUID, uuid4

from application.commands import (
CreateCartCommand, AddItemCommand, ChangeQuantityCommand,
RemoveItemCommand, ApplyDiscountCommand, ClearCartCommand, CheckoutCommand,
)
from application.handlers import CartCommandHandler
from infrastructure.event_store import EventStore
from infrastructure.snapshot_store import SnapshotStore
from infrastructure.projections import CartProjector


def print_separator(title: str) -> None:
print()
print("=" * 65)
print(f" {title}")
print("=" * 65)


def main() -> None:
print("🛒 EVENT SOURCING — Shopping Cart Demo")
print("=" * 65)

# Khởi tạo infrastructure
event_store = EventStore()
snapshot_store = SnapshotStore()
projector = CartProjector()
handler = CartCommandHandler(event_store, snapshot_store, projector)

customer_id = UUID("12345678-1234-5678-1234-567812345678")

# === 1. Tạo giỏ hàng ===
print_separator("1. Tạo giỏ hàng mới")
result = handler.handle_create_cart(CreateCartCommand(customer_id=customer_id))
cart_id = UUID(result["cart_id"])
print(f" ✅ Đã tạo giỏ hàng: {cart_id}")
print(f" 📦 Events appended: {result['events_appended']}")
print(f" 📌 Trạng thái: {event_store.count_events(cart_id)} events")

# === 2. Thêm sản phẩm ===
print_separator("2. Thêm sản phẩm vào giỏ")
items = [
("iPhone 15 Pro Max", Decimal("34990000"), 1),
("AirPods Pro 2", Decimal("6490000"), 2),
("Ốp lưng Silicon", Decimal("299000"), 3),
]
for name, price, qty in items:
result = handler.handle_add_item(AddItemCommand(
cart_id=cart_id,
product_id=uuid4(),
product_name=name,
price=price,
quantity=qty,
))
print(f" ✅ + {name:25s} x{qty:2d} = {price * Decimal(str(qty)):>12,.0f}₫")
print(f" Tổng: {result['total']:>25,.0f}₫ | Items: {result['item_count']:2d}")

# === 3. Đọc trạng thái hiện tại ===
print_separator("3. Replay events → Current State")
cart = handler._load_cart(cart_id)
print(f" 🛒 Giỏ hàng: {cart.id}")
print(f" 👤 Khách: {cart.customer_id}")
print(f" 📦 Số sản phẩm: {cart.unique_items} loại, {cart.item_count} cái")
print(f" 💰 Tổng tiền: {cart.calculate_total():>25,.0f}₫")
print(f" 📌 Version: {cart.version}")

for pid, item in cart.items.items():
print(f" • {item.product_name:25s} x{item.quantity} = {item.subtotal:>12,.0f}₫")

# === 4. Thay đổi số lượng ===
print_separator("4. Thay đổi số lượng (Command + Event)")
first_product_id = list(cart.items.keys())[0]
result = handler.handle_change_quantity(ChangeQuantityCommand(
cart_id=cart_id,
product_id=first_product_id,
new_quantity=3,
))
print(f" ✅ Đã thay đổi số lượng sản phẩm đầu tiên từ 1 → 3")
print(f" 📦 Tổng items: {result['item_count']}")
print(f" 💰 Tổng tiền: {result['total']:>25,.0f}₫")

# === 5. Áp dụng mã giảm giá ===
print_separator("5. Áp dụng mã giảm giá (Event)")
total_before = handler._load_cart(cart_id).calculate_total()
print(f" 💵 Tổng trước giảm: {total_before:>25,.0f}₫")

result = handler.handle_apply_discount(ApplyDiscountCommand(
cart_id=cart_id,
coupon_code="SALE10",
amount=Decimal("10"),
discount_type="PERCENTAGE",
))
print(f" 🏷️ Mã: SALE10 (giảm 10%)")
print(f" 💰 Tổng sau giảm: {result['total']:>25,.0f}₫")

# === 6. Xóa sản phẩm ===
print_separator("6. Xóa sản phẩm khỏi giỏ (Event)")
second_product_id = list(cart.items.keys())[1]
result = handler.handle_remove_item(RemoveItemCommand(
cart_id=cart_id,
product_id=second_product_id,
))
print(f" ✅ Đã xóa sản phẩm thứ hai khỏi giỏ")
print(f" 📦 Items còn lại: {result['item_count']}")
print(f" 💰 Tổng: {result['total']:>25,.0f}₫")

# === 7. Lịch sử events ===
print_separator("7. Event Stream (Immutable History)")
events = event_store.get_events(cart_id)
for i, event in enumerate(events, 1):
event_type = type(event).__name__
timestamp = event.timestamp.strftime("%H:%M:%S.%f")[:-3]
details = _get_event_details(event)
print(f" {i:2d}. [{timestamp}] {event_type:25s} {details}")

# === 8. Kiểm tra Projection ===
print_separator("8. Read Model (CartProjection)")
projection = projector.get_cart_projection(cart_id)
if projection:
print(f" 🆔 Cart: {projection.cart_id}")
print(f" 👤 Customer: {projection.customer_id}")
print(f" 📦 Items: {projection.unique_items} loại, {projection.item_count} cái")
print(f" 💰 Total: {projection.total:>25,.0f}₫")
print(f" 🏷️ Discount: {projection.applied_discount}")
print(f" ✅ Checked out: {projection.is_checked_out}")
print()
for item in projection.items:
print(f" • {item['product_name']:25s} x{item['quantity']} = {item['subtotal']:>12,.0f}₫")

# === 9. Customer Summary ===
print_separator("9. Customer Summary (Rebuilt from Events)")
summary = projector.get_customer_summary(customer_id)
if summary:
print(f" 👤 Customer: {summary.customer_id}")
print(f" 🛒 Active carts: {summary.active_carts}")
print(f" 📦 Total items: {summary.total_items_in_carts}")
print(f" 💰 Total value: {summary.total_value:>25,.0f}₫")

# === 10. Rebuild từ events ===
print_separator("10. Rebuild Projections từ Events (Idempotent)")
all_events = event_store.get_all_events()
print(f" Tổng events: {len(all_events)}")
projector.rebuild_from_events(all_events)
rebuilt = projector.get_cart_projection(cart_id)
print(f" ✅ Rebuilt projection: {rebuilt.unique_items} items, {rebuilt.total:,.0f}₫")
print(f" Kiểm tra consistency: projection.total == load_cart().total?")
cart = handler._load_cart(cart_id)
assert rebuilt.total == float(cart.calculate_total()), "Consistency check FAILED!"
print(f" ✅ Consistency check PASSED!")

# === 11. Thanh toán ===
print_separator("11. Checkout (Event cuối cùng)")
result = handler.handle_checkout(CheckoutCommand(
cart_id=cart_id,
payment_method="CREDIT_CARD",
shipping_address="123 Nguyễn Huệ, Quận 1, TP. Hồ Chí Minh",
))
print(f" ✅ Đã thanh toán: {result['total']:>25,.0f}₫")
print(f" 📦 Items: {result['item_count']}")

# === 12. Tính bất biến của Event ===
print_separator("12. Event Immutability & Temporal Query")
print(f" Tổng events trong Event Store: {len(event_store.get_all_events())}")
print(f" Các events KHÔNG THỂ bị sửa hoặc xóa.")
print(f" Để sửa lỗi, ta tạo event mới (corrective event).")
print(f" Luôn có thể replay từ đầu để rebuild bất kỳ trạng thái nào.")

print()
print("=" * 65)
print(" ✅ Event Sourcing Demo hoàn tất!")
print("=" * 65)


def _get_event_details(event) -> str:
"""Lấy thông tin chi tiết ngắn gọn từ event."""
if hasattr(event, 'product_name'):
return f"{getattr(event, 'product_name', '')} x{getattr(event, 'quantity', '')}"
if hasattr(event, 'coupon_code'):
return f"Code: {event.coupon_code} ({event.discount_type})"
if hasattr(event, 'total_amount'):
return f"Total: {event.total_amount:,.0f}₫"
if hasattr(event, 'reason'):
return f"Reason: {event.reason}"
return ""


if __name__ == "__main__":
main()

Output khi chạy:

🛒 EVENT SOURCING — Shopping Cart Demo
=================================================================

=================================================================
1. Tạo giỏ hàng mới
=================================================================
✅ Đã tạo giỏ hàng: 550e8400-e29b-41d4-a716-446655440000
📦 Events appended: 1
📌 Trạng thái: 1 events

=================================================================
2. Thêm sản phẩm vào giỏ
=================================================================
✅ + iPhone 15 Pro Max x 1 = 34,990,000₫
Tổng: 34,990,000₫ | Items: 1
✅ + AirPods Pro 2 x 2 = 12,980,000₫
Tổng: 47,970,000₫ | Items: 3
✅ + Ốp lưng Silicon x 3 = 897,000₫
Tổng: 48,867,000₫ | Items: 6

=================================================================
3. Replay events → Current State
=================================================================
🛒 Giỏ hàng: 550e8400-e29b-41d4-a716-446655440000
👤 Khách: 12345678-1234-5678-1234-567812345678
📦 Số sản phẩm: 3 loại, 6 cái
💰 Tổng tiền: 48,867,000₫
📌 Version: 4
• iPhone 15 Pro Max x1 = 34,990,000₫
• AirPods Pro 2 x2 = 12,980,000₫
• Ốp lưng Silicon x3 = 897,000₫

=================================================================
4. Thay đổi số lượng (Command + Event)
=================================================================
✅ Đã thay đổi số lượng sản phẩm đầu tiên từ 1 → 3
📦 Tổng items: 8
💰 Tổng tiền: 118,847,000₫

=================================================================
5. Áp dụng mã giảm giá (Event)
=================================================================
💵 Tổng trước giảm: 118,847,000₫
🏷️ Mã: SALE10 (giảm 10%)
💰 Tổng sau giảm: 106,962,300₫

=================================================================
6. Xóa sản phẩm khỏi giỏ (Event)
=================================================================
✅ Đã xóa sản phẩm thứ hai khỏi giỏ
📦 Items còn lại: 6
💰 Tổng: 96,291,000₫

=================================================================
7. Event Stream (Immutable History)
=================================================================
1. [timestamp] CartCreated Customer: 12345678-1234-5678-1234-567812345678
2. [timestamp] ItemAddedToCart iPhone 15 Pro Max x1
3. [timestamp] ItemAddedToCart AirPods Pro 2 x2
4. [timestamp] ItemAddedToCart Ốp lưng Silicon x3
5. [timestamp] ItemQuantityChanged iPhone 15 Pro Max: 1→3
6. [timestamp] DiscountApplied Code: SALE10 (PERCENTAGE)
7. [timestamp] ItemRemovedFromCart AirPods Pro 2 removed
8. [timestamp] CartCheckedOut Total: 96,291,000₫

=================================================================
8. Read Model (CartProjection)
=================================================================
🆔 Cart: 550e8400-e29b-41d4-a716-446655440000
👤 Customer: 12345678-1234-5678-1234-567812345678
📦 Items: 2 loại, 6 cái
💰 Total: 96,291,000₫
🏷️ Discount: {'code': 'SALE10', 'amount': 10.0, 'type': 'PERCENTAGE'}
✅ Checked out: True
• iPhone 15 Pro Max x3 = 104,970,000₫
• Ốp lưng Silicon x3 = 897,000₫

=================================================================
9. Customer Summary (Rebuilt from Events)
=================================================================
👤 Customer: 12345678-1234-5678-1234-567812345678
🛒 Active carts: 0
📦 Total items: 0
💰 Total value: 0₫

=================================================================
10. Rebuild Projections từ Events (Idempotent)
=================================================================
Tổng events: 8
✅ Rebuilt projection: 2 items, 96,291,000₫
Kiểm tra consistency: projection.total == load_cart().total?
✅ Consistency check PASSED!

=================================================================
11. Checkout (Event cuối cùng)
=================================================================
✅ Đã thanh toán: 96,291,000₫
📦 Items: 6

=================================================================
12. Event Immutability & Temporal Query
=================================================================
Tổng events trong Event Store: 8
Các events KHÔNG THỂ bị sửa hoặc xóa.
Để sửa lỗi, ta tạo event mới (corrective event).
Luôn có thể replay từ đầu để rebuild bất kỳ trạng thái nào.

=================================================================
✅ Event Sourcing Demo hoàn tất!
=================================================================

Khi nào dùng / Khi nào không

Khi nào dùng Event SourcingKhi nào không dùng
Cần audit trail đầy đủ, không thể mất lịch sửCRUD đơn giản, không cần lịch sử
Cần temporal queries (trạng thái tại thời điểm bất kỳ)Hệ thống real-time, cần strong consistency
Domain phức tạp, nhiều business eventsStorage không phải là vấn đề
Cần debug/replay khả năng caoSố lượng events quá lớn, performance quan trọng
Hệ thống event-driven, CQRSTeam chưa quen với event-driven thinking
Machine learning trên event streamChi phí storage là ưu tiên số 1
Compliance/Regulatory requirementsCần xóa dữ liệu vĩnh viễn (GDPR right to erasure)

Ưu điểm / Nhược điểm

Ưu điểmNhược điểm
Audit trail hoàn hảo, biết chính xác ai làm gìPhức tạp hơn CRUD rất nhiều
Temporal query: biết trạng thái tại bất kỳ thời điểm nàoEvent schema evolution khó khăn
Replay để debug, tái tạo bugStorage lớn hơn (lưu tất cả events)
Event stream cho ML/AIEventually consistency
CQRS tự nhiên: events là nguồn cho read modelCần snapshot cho performance
Không mất dữ liệu (append-only)Khó xóa dữ liệu (immutable)
Parallel development dễ dàngCần kỷ luật về event design
High availability (event store phân tán)Testing phức tạp hơn

Công cụ và Framework

Event Store chuyên dụng

  • EventStoreDB — CSDL events chuyên dụng, có giao diện HTTP/Protobuf
  • Axon Server — Event store + message bus cho Java/Axon Framework
  • Kafka — Distributed event log, có thể dùng làm event store
  • Pulsar — Event streaming platform

Python

  • Eventsourcing — Thư viện Python đầy đủ cho Event Sourcing + CQRS
  • EventStoreDB Client — Python client cho EventStoreDB
  • Kafka-Python — Kafka client cho Python
  • Faust — Stream processing library

.NET

  • EventStore.Client — .NET client cho EventStoreDB
  • Martendb — Event store + document DB cho .NET
  • NEventStore — Event store library cho .NET
  • SqlStreamStore — Event store trên SQL database

Java

  • Axon Framework — CQRS + Event Sourcing full-stack
  • Eventuate — Microservices với ES
  • Lagom — Reactive microservices với ES

Storage Options

  • PostgreSQL — Dùng event table + JSONB
  • MySQL/MariaDB — Tương tự PostgreSQL
  • MongoDB — Document store phù hợp với event
  • DynamoDB — AWS native event store
  • Google Spanner — Globally distributed

Kiểm thử

Chiến lược kiểm thử cho Event Sourcing

# tests/test_cart.py

from __future__ import annotations

from decimal import Decimal
from uuid import uuid4

import pytest

from domain.cart import Cart, CartError, CartEmptyError, ProductNotInCartError
from domain.events import (
CartCreated, ItemAddedToCart, ItemRemovedFromCart,
CartCleared, CartCheckedOut,
)


class TestCartAggregate:
"""Kiểm thử Aggregate — trung tâm của Event Sourcing."""

@pytest.fixture
def cart_id(self):
return uuid4()

@pytest.fixture
def customer_id(self):
return uuid4()

def test_create_cart(self, customer_id):
cart, events = Cart.create(customer_id)
assert len(events) == 1
assert isinstance(events[0], CartCreated)
assert cart.customer_id == customer_id
assert cart.item_count == 0

def test_add_item(self, customer_id):
cart, _ = Cart.create(customer_id)
product_id = uuid4()
events = cart.add_item(
product_id=product_id,
product_name="Test Product",
price=Decimal("100000"),
quantity=2,
)
assert len(events) == 1
assert isinstance(events[0], ItemAddedToCart)
cart.apply(events[0])
assert cart.item_count == 2
assert cart.calculate_total() == Decimal("200000")

def test_add_zero_quantity(self, customer_id):
cart, _ = Cart.create(customer_id)
with pytest.raises(CartError, match="lớn hơn 0"):
cart.add_item(
product_id=uuid4(),
product_name="Test",
price=Decimal("100000"),
quantity=0,
)

def test_remove_item(self, customer_id):
cart, _ = Cart.create(customer_id)
pid = uuid4()
events = cart.add_item(pid, "Test", Decimal("50000"), 1)
cart.apply(events[0])

events = cart.remove_item(pid)
assert len(events) == 1
assert isinstance(events[0], ItemRemovedFromCart)
cart.apply(events[0])
assert cart.item_count == 0

def test_remove_nonexistent_item(self, customer_id):
cart, _ = Cart.create(customer_id)
with pytest.raises(ProductNotInCartError):
cart.remove_item(uuid4())

def test_checkout_empty_cart(self, customer_id):
cart, _ = Cart.create(customer_id)
with pytest.raises(CartEmptyError):
cart.checkout("CREDIT_CARD", "Address")

def test_cannot_modify_after_checkout(self, customer_id):
cart, _ = Cart.create(customer_id)
pid = uuid4()
events = cart.add_item(pid, "Test", Decimal("50000"), 1)
cart.apply(events[0])

cart.checkout("CASH", "Address")
cart.apply = lambda e: None # Ứng xử như đã checkout

with pytest.raises(CartError, match="đã thanh toán"):
cart.add_item(pid, "Test", Decimal("50000"), 1)

def test_calculate_total_with_discount(self, customer_id):
cart, _ = Cart.create(customer_id)
pid = uuid4()
events = cart.add_item(pid, "Laptop", Decimal("20000000"), 1)
cart.apply(events[0])

# Percentage discount 10%
events = cart.apply_discount("SALE10", Decimal("10"), "PERCENTAGE")
cart.apply(events[0])
assert cart.calculate_total() == Decimal("18000000")

# Fixed discount
cart.applied_discount = None
events = cart.apply_discount("GIAM2TR", Decimal("2000000"), "FIXED")
cart.apply(events[0])
assert cart.calculate_total() == Decimal("18000000")


# tests/test_event_store.py

from __future__ import annotations

from decimal import Decimal
from uuid import uuid4

import pytest

from domain.events import CartCreated, ItemAddedToCart
from infrastructure.event_store import EventStore, ConcurrencyError


class TestEventStore:
"""Kiểm thử Event Store — append-only, concurrency."""

@pytest.fixture
def event_store(self):
return EventStore()

def test_append_and_read_events(self, event_store: EventStore):
aggregate_id = uuid4()
events = [
CartCreated(aggregate_id=aggregate_id, customer_id=uuid4()),
ItemAddedToCart(
aggregate_id=aggregate_id,
product_id=uuid4(),
product_name="Test",
price=Decimal("100000"),
quantity=1,
),
]
event_store.append(aggregate_id, events, 0)

retrieved = event_store.get_events(aggregate_id)
assert len(retrieved) == 2
assert isinstance(retrieved[0], CartCreated)
assert isinstance(retrieved[1], ItemAddedToCart)

def test_optimistic_concurrency(self, event_store: EventStore):
aggregate_id = uuid4()
events = [CartCreated(aggregate_id=aggregate_id, customer_id=uuid4())]
event_store.append(aggregate_id, events, 0)

# Conflict: expected version 0 nhưng đã có 1 event
with pytest.raises(ConcurrencyError):
event_store.append(aggregate_id, events, 0)

def test_append_multiple_batches(self, event_store: EventStore):
aggregate_id = uuid4()
event1 = [CartCreated(aggregate_id=aggregate_id, customer_id=uuid4())]
event_store.append(aggregate_id, event1, 0)

event2 = [ItemAddedToCart(
aggregate_id=aggregate_id,
product_id=uuid4(),
product_name="Test",
price=Decimal("50000"),
quantity=1,
)]
event_store.append(aggregate_id, event2, 1) # version 1

assert event_store.count_events(aggregate_id) == 2


# tests/test_projections.py

from __future__ import annotations

from decimal import Decimal
from uuid import uuid4

import pytest

from domain.events import CartCreated, ItemAddedToCart, DiscountApplied
from infrastructure.projections import CartProjector


class TestCartProjector:
"""Kiểm thử Projector — event → read model."""

@pytest.fixture
def projector(self):
return CartProjector()

def test_cart_created_projection(self, projector: CartProjector):
cart_id = uuid4()
event = CartCreated(aggregate_id=cart_id, customer_id=uuid4())
projector.project(event)

projection = projector.get_cart_projection(cart_id)
assert projection is not None
assert projection.item_count == 0
assert projection.total == Decimal("0")

def test_item_added_projection(self, projector: CartProjector):
cart_id = uuid4()
projector.project(CartCreated(aggregate_id=cart_id, customer_id=uuid4()))

event = ItemAddedToCart(
aggregate_id=cart_id,
product_id=uuid4(),
product_name="Test",
price=Decimal("100000"),
quantity=3,
)
projector.project(event)

projection = projector.get_cart_projection(cart_id)
assert projection.item_count == 3
assert projection.total == Decimal("300000")
assert len(projection.items) == 1

def test_rebuild_is_idempotent(self, projector: CartProjector):
"""Rebuild from same events → same result."""
cart_id = uuid4()
events = [
CartCreated(aggregate_id=cart_id, customer_id=uuid4()),
ItemAddedToCart(
aggregate_id=cart_id,
product_id=uuid4(),
product_name="Product A",
price=Decimal("50000"),
quantity=2,
),
]

projector.rebuild_from_events(events)
first = projector.get_cart_projection(cart_id)

projector.rebuild_from_events(events)
second = projector.get_cart_projection(cart_id)

assert first.total == second.total
assert first.item_count == second.item_count

Kết luận

Event Sourcing là một pattern mạnh mẽ thay đổi cách bạn nghĩ về dữ liệu. Thay vì lưu "trạng thái hiện tại", bạn lưu "câu chuyện" — mọi sự kiện đã xảy ra. Điều này mang lại khả năng audit, temporal query, và debug không thể có với CRUD.

Best Practices

  1. Event names are past tenseOrderPlaced, ItemAdded, MoneyWithdrawn
  2. Events are business facts — Không lưu technical detail trong event
  3. One event = one change — Event nhỏ, atomic
  4. Schema evolution plan — Dùng event versioning, không sửa event cũ
  5. Snapshot strategy — Snapshot khi cần, không snapshot quá thường xuyên
  6. Idempotent projections — Chạy lại projection nhiều lần cho kết quả giống nhau
  7. Eventual consistency accepted — Ghi rõ trong API documentation
  8. Testing bằng event replay — Test bằng cách replay events

Golden Rules

RuleMô tả
Events are immutableKhông bao giờ sửa hoặc xóa event đã lưu
Current state is derivedState hiện tại là kết quả của replay events
Event store is source of truthMọi thứ khác đều là cache
Correct with new eventsSai thì tạo event mới, không sửa event cũ
Schema evolution is additiveThêm field, không xóa field
Projections are disposableCó thể xóa và rebuild bất kỳ lúc nào
Aggregate = consistency boundaryTransaction chỉ trong một aggregate

Khi nào Event Sourcing thực sự tỏa sáng

  • Hệ thống tài chính: Banking, insurance, trading
  • Hệ thống compliance: Healthcare, government, legal
  • E-commerce: Order management, inventory, cart
  • IoT: Device state history, sensor data
  • Audit-heavy systems: Any system where "who changed what and when" matters

Event Sourcing không phải là giải pháp cho mọi bài toán. Nhưng khi bạn cần biết chính xác điều gì đã xảy ra, không chỉ trạng thái hiện tại, thì Event Sourcing là lựa chọn đúng đắn.