helland-distributed-data
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePat Helland Style Guide
Pat Helland 风格设计指南
Overview
概述
Pat Helland has worked on distributed systems for 40+ years at Tandem (fault-tolerant transaction systems), Microsoft (SQL Server, Cosmos DB), and Amazon. His papers on scalable data patterns—especially "Life Beyond Distributed Transactions"—have shaped how the industry builds large-scale systems.
Pat Helland在分布式系统领域深耕40余年,曾任职于Tandem(容错事务系统)、微软(SQL Server、Cosmos DB)和亚马逊。他关于可扩展数据模式的论文——尤其是《Life Beyond Distributed Transactions》(超越分布式事务的设计)——深刻影响了行业构建大规模系统的方式。
Core Philosophy
核心理念
"In a world with unbounded scale, you cannot have distributed transactions."
"Idempotency is the key to building reliable systems."
"Data on the inside is not the same as data on the outside."
Helland believes that as systems scale, traditional ACID transactions become impractical. Instead, we need new patterns: idempotent operations, entity-based partitioning, and application-level consistency.
“在无限扩展的场景中,分布式事务不再可行。”
“幂等性是构建可靠系统的关键。”
“内部数据与外部数据并非同一概念。”
Helland认为,随着系统规模扩大,传统的ACID事务会变得不切实际。取而代之的是,我们需要新的模式:幂等操作、基于实体的分区,以及应用层一致性。
Design Principles
设计原则
-
Entities, Not Tables: Think in terms of independently scalable entities, not relational tables.
-
Idempotency Everywhere: Operations must be safely retryable.
-
Messages, Not Transactions: Cross-entity consistency happens via messaging, not 2PC.
-
Scale Agnosticism: Design as if you don't know (or care) how many nodes exist.
-
Inside vs Outside Data: Internal data is mutable and rich; external data is immutable and simple.
-
实体优先,而非表:以可独立扩展的实体为思考单位,而非关系型表。
-
处处实现幂等性:操作必须能够安全重试。
-
消息驱动,而非事务:跨实体一致性通过消息传递实现,而非两阶段提交(2PC)。
-
与规模无关的设计:设计时无需考虑(或在意)系统包含多少节点。
-
内部数据与外部数据分离:内部数据可变且丰富;外部数据不可变且简洁。
When Designing Distributed Data Systems
分布式数据系统设计实践
Always
必须遵循
- Design entities that can be independently scaled and partitioned
- Make all operations idempotent (same request twice = same result)
- Use unique request IDs to detect and deduplicate retries
- Accept that cross-entity operations are eventually consistent
- Version your external data contracts
- Plan for messages to be delivered at-least-once
- 设计可独立扩展和分区的实体
- 确保所有操作具备幂等性(同一请求执行两次,结果完全相同)
- 使用唯一请求ID检测并去重重试请求
- 接受跨实体操作最终一致的特性
- 为外部数据契约添加版本控制
- 按至少一次投递的标准设计消息机制
Never
绝对避免
- Depend on distributed transactions for correctness at scale
- Assume exactly-once message delivery
- Share mutable state across service boundaries
- Design entities that require coordination with other entities for basic operations
- Ignore the CAP theorem implications of your design
- 在大规模场景下依赖分布式事务保证正确性
- 假设消息会被精确投递一次
- 在服务边界之间共享可变状态
- 设计基础操作需要与其他实体协调的实体
- 忽略设计对CAP定理的影响
Prefer
优先选择
- Idempotent operations over exactly-once semantics
- Event sourcing over mutable state
- Saga pattern over 2PC
- Entity-based partitioning over arbitrary sharding
- Immutable messages over mutable shared state
- 幂等操作优于精确一次语义
- 事件溯源优于可变状态
- Saga模式优于2PC
- 基于实体的分区优于任意分片
- 不可变消息优于可变共享状态
Code Patterns
代码模式
Idempotent Operations
幂等操作
python
class IdempotentPaymentService:
"""
Helland's key insight: if operations are idempotent,
retries are safe, and you don't need exactly-once delivery.
"""
def __init__(self, db):
self.db = db
def process_payment(self, request_id: str, account_id: str, amount: Decimal):
# Check if we've already processed this request
existing = self.db.get_processed_request(request_id)
if existing:
return existing.result # Return same result as before
# Process the payment
with self.db.transaction():
account = self.db.get_account(account_id)
account.balance -= amount
result = PaymentResult(
request_id=request_id,
status='completed',
new_balance=account.balance
)
# Record that we processed this request (atomically with the change)
self.db.save_processed_request(request_id, result)
self.db.save_account(account)
return resultpython
class IdempotentPaymentService:
"""
Helland's key insight: if operations are idempotent,
retries are safe, and you don't need exactly-once delivery.
"""
def __init__(self, db):
self.db = db
def process_payment(self, request_id: str, account_id: str, amount: Decimal):
# Check if we've already processed this request
existing = self.db.get_processed_request(request_id)
if existing:
return existing.result # Return same result as before
# Process the payment
with self.db.transaction():
account = self.db.get_account(account_id)
account.balance -= amount
result = PaymentResult(
request_id=request_id,
status='completed',
new_balance=account.balance
)
# Record that we processed this request (atomically with the change)
self.db.save_processed_request(request_id, result)
self.db.save_account(account)
return resultEntity-Based Design
基于实体的设计
python
class Order:
"""
Helland's entity pattern: each entity is an island of consistency.
Cross-entity operations happen via messaging, not transactions.
"""
def __init__(self, order_id: str):
self.order_id = order_id
self.items = []
self.status = 'pending'
self.version = 0
# Outbox: messages to send (part of entity's transaction)
self.outbox = []
def add_item(self, product_id: str, quantity: int, request_id: str):
"""All mutations include request_id for idempotency."""
if self.has_processed(request_id):
return # Already did this
self.items.append(OrderItem(product_id, quantity))
self.record_processed(request_id)
self.version += 1
def submit(self, request_id: str):
if self.has_processed(request_id):
return
self.status = 'submitted'
self.record_processed(request_id)
self.version += 1
# Queue message for inventory service (not a distributed txn!)
self.outbox.append(Message(
type='OrderSubmitted',
order_id=self.order_id,
items=self.items
))python
class Order:
"""
Helland's entity pattern: each entity is an island of consistency.
Cross-entity operations happen via messaging, not transactions.
"""
def __init__(self, order_id: str):
self.order_id = order_id
self.items = []
self.status = 'pending'
self.version = 0
# Outbox: messages to send (part of entity's transaction)
self.outbox = []
def add_item(self, product_id: str, quantity: int, request_id: str):
"""All mutations include request_id for idempotency."""
if self.has_processed(request_id):
return # Already did this
self.items.append(OrderItem(product_id, quantity))
self.record_processed(request_id)
self.version += 1
def submit(self, request_id: str):
if self.has_processed(request_id):
return
self.status = 'submitted'
self.record_processed(request_id)
self.version += 1
# Queue message for inventory service (not a distributed txn!)
self.outbox.append(Message(
type='OrderSubmitted',
order_id=self.order_id,
items=self.items
))Inside Data vs Outside Data
内部数据与外部数据分离
python
undefinedpython
undefinedINSIDE DATA: Rich, mutable, internal representation
INSIDE DATA: Rich, mutable, internal representation
class InternalOrder:
order_id: str
customer: Customer # Full customer object
items: List[OrderItem] # Mutable list
shipping_address: Address # Complex nested object
internal_notes: str # Internal-only field
audit_log: List[AuditEntry] # Full history
version: int # Optimistic concurrency
def to_external(self) -> 'ExternalOrder':
"""Convert to outside representation for APIs/messages."""
return ExternalOrder(
order_id=self.order_id,
customer_id=self.customer.id, # Just the ID, not full object
item_ids=[i.id for i in self.items], # Just IDs
submitted_at=self.audit_log[0].timestamp # Simplified
)class InternalOrder:
order_id: str
customer: Customer # Full customer object
items: List[OrderItem] # Mutable list
shipping_address: Address # Complex nested object
internal_notes: str # Internal-only field
audit_log: List[AuditEntry] # Full history
version: int # Optimistic concurrency
def to_external(self) -> 'ExternalOrder':
"""Convert to outside representation for APIs/messages."""
return ExternalOrder(
order_id=self.order_id,
customer_id=self.customer.id, # Just the ID, not full object
item_ids=[i.id for i in self.items], # Just IDs
submitted_at=self.audit_log[0].timestamp # Simplified
)OUTSIDE DATA: Simple, immutable, versioned contract
OUTSIDE DATA: Simple, immutable, versioned contract
@dataclass(frozen=True) # Immutable!
class ExternalOrder:
"""
Helland's rule: data on the outside is:
- Immutable (represents a point in time)
- Versioned (schema can evolve)
- Simple (no complex nested structures)
- Self-describing (includes type info)
"""
order_id: str
customer_id: str
item_ids: List[str]
submitted_at: datetime
schema_version: str = "1.0"
undefined@dataclass(frozen=True) # Immutable!
class ExternalOrder:
"""
Helland's rule: data on the outside is:
- Immutable (represents a point in time)
- Versioned (schema can evolve)
- Simple (no complex nested structures)
- Self-describing (includes type info)
"""
order_id: str
customer_id: str
item_ids: List[str]
submitted_at: datetime
schema_version: str = "1.0"
undefinedSaga Pattern (Instead of Distributed Transactions)
Saga模式(替代分布式事务)
python
class OrderSaga:
"""
Helland's alternative to 2PC: sagas with compensating actions.
Each step is a local transaction + message to next step.
Failures trigger compensating transactions.
"""
def __init__(self, order_id: str):
self.order_id = order_id
self.state = 'started'
self.completed_steps = []
async def execute(self):
try:
# Step 1: Reserve inventory (local txn in Inventory service)
await self.reserve_inventory()
self.completed_steps.append('inventory_reserved')
# Step 2: Charge payment (local txn in Payment service)
await self.charge_payment()
self.completed_steps.append('payment_charged')
# Step 3: Ship order (local txn in Shipping service)
await self.ship_order()
self.completed_steps.append('order_shipped')
self.state = 'completed'
except Exception as e:
# Compensate in reverse order
await self.compensate()
self.state = 'compensated'
raise
async def compensate(self):
"""Undo completed steps in reverse order."""
for step in reversed(self.completed_steps):
if step == 'order_shipped':
await self.cancel_shipment()
elif step == 'payment_charged':
await self.refund_payment()
elif step == 'inventory_reserved':
await self.release_inventory()python
class OrderSaga:
"""
Helland's alternative to 2PC: sagas with compensating actions.
Each step is a local transaction + message to next step.
Failures trigger compensating transactions.
"""
def __init__(self, order_id: str):
self.order_id = order_id
self.state = 'started'
self.completed_steps = []
async def execute(self):
try:
# Step 1: Reserve inventory (local txn in Inventory service)
await self.reserve_inventory()
self.completed_steps.append('inventory_reserved')
# Step 2: Charge payment (local txn in Payment service)
await self.charge_payment()
self.completed_steps.append('payment_charged')
# Step 3: Ship order (local txn in Shipping service)
await self.ship_order()
self.completed_steps.append('order_shipped')
self.state = 'completed'
except Exception as e:
# Compensate in reverse order
await self.compensate()
self.state = 'compensated'
raise
async def compensate(self):
"""Undo completed steps in reverse order."""
for step in reversed(self.completed_steps):
if step == 'order_shipped':
await self.cancel_shipment()
elif step == 'payment_charged':
await self.refund_payment()
elif step == 'inventory_reserved':
await self.release_inventory()Outbox Pattern for Reliable Messaging
实现可靠消息的Outbox模式
python
class OutboxPublisher:
"""
Helland's insight: you can't atomically update DB and send a message.
Solution: write message to outbox table in same transaction,
then publish from outbox asynchronously.
"""
def __init__(self, db, message_broker):
self.db = db
self.broker = message_broker
def update_with_message(self, entity, message):
"""Atomically update entity and queue message."""
with self.db.transaction():
self.db.save(entity)
self.db.insert_outbox(OutboxEntry(
id=uuid4(),
message=message,
status='pending',
created_at=datetime.utcnow()
))
async def publish_outbox(self):
"""Background process: publish pending messages."""
while True:
pending = self.db.get_pending_outbox_entries(limit=100)
for entry in pending:
try:
await self.broker.publish(entry.message)
self.db.mark_outbox_published(entry.id)
except Exception:
# Will retry on next iteration
pass
await asyncio.sleep(1)python
class OutboxPublisher:
"""
Helland's insight: you can't atomically update DB and send a message.
Solution: write message to outbox table in same transaction,
then publish from outbox asynchronously.
"""
def __init__(self, db, message_broker):
self.db = db
self.broker = message_broker
def update_with_message(self, entity, message):
"""Atomically update entity and queue message."""
with self.db.transaction():
self.db.save(entity)
self.db.insert_outbox(OutboxEntry(
id=uuid4(),
message=message,
status='pending',
created_at=datetime.utcnow()
))
async def publish_outbox(self):
"""Background process: publish pending messages."""
while True:
pending = self.db.get_pending_outbox_entries(limit=100)
for entry in pending:
try:
await self.broker.publish(entry.message)
self.db.mark_outbox_published(entry.id)
except Exception:
# Will retry on next iteration
pass
await asyncio.sleep(1)Request-Response with Correlation
带关联ID的请求-响应模式
python
class AsyncRequestResponse:
"""
Helland's pattern for async request-response:
include correlation ID, expect response via messaging.
"""
def __init__(self, outbox, response_handler):
self.outbox = outbox
self.pending_requests = {}
self.response_handler = response_handler
async def send_request(self, target_service: str, payload: dict) -> str:
correlation_id = str(uuid4())
request = Message(
correlation_id=correlation_id,
reply_to='my-service-responses',
target=target_service,
payload=payload
)
self.pending_requests[correlation_id] = {
'sent_at': datetime.utcnow(),
'request': request
}
await self.outbox.publish(request)
return correlation_id
async def handle_response(self, message: Message):
correlation_id = message.correlation_id
if correlation_id in self.pending_requests:
original = self.pending_requests.pop(correlation_id)
await self.response_handler(original['request'], message)python
class AsyncRequestResponse:
"""
Helland's pattern for async request-response:
include correlation ID, expect response via messaging.
"""
def __init__(self, outbox, response_handler):
self.outbox = outbox
self.pending_requests = {}
self.response_handler = response_handler
async def send_request(self, target_service: str, payload: dict) -> str:
correlation_id = str(uuid4())
request = Message(
correlation_id=correlation_id,
reply_to='my-service-responses',
target=target_service,
payload=payload
)
self.pending_requests[correlation_id] = {
'sent_at': datetime.utcnow(),
'request': request
}
await self.outbox.publish(request)
return correlation_id
async def handle_response(self, message: Message):
correlation_id = message.correlation_id
if correlation_id in self.pending_requests:
original = self.pending_requests.pop(correlation_id)
await self.response_handler(original['request'], message)Mental Model
思维模型
Helland approaches distributed data design by asking:
- What are the entities? What are the natural units of consistency?
- How do entities interact? Via messages, not shared transactions
- What if this message is delivered twice? Design for idempotency
- What if this operation fails halfway? Design compensating actions
- What data crosses boundaries? Keep it simple and immutable
Helland在设计分布式数据系统时会提出以下问题:
- 核心实体是什么? 天然的一致性单元是什么?
- 实体如何交互? 通过消息传递,而非共享事务
- 如果消息被投递两次会怎样? 按幂等性设计
- 如果操作中途失败会怎样? 设计补偿操作
- 哪些数据会跨边界传递? 保持数据简洁且不可变
Signature Helland Moves
Helland的标志性设计方法
- Idempotent operations with request IDs
- Entity-based partitioning
- Outbox pattern for reliable messaging
- Saga pattern instead of 2PC
- Inside data vs outside data distinction
- At-least-once delivery with deduplication
- Immutable external data contracts
- 带请求ID的幂等操作
- 基于实体的分区
- 实现可靠消息的Outbox模式
- 替代2PC的Saga模式
- 内部数据与外部数据的分离
- 至少一次投递+去重
- 不可变的外部数据契约
Key Papers
关键论文
- "Life Beyond Distributed Transactions: An Apostate's Opinion" (2007, 2016)
- "Data on the Outside vs Data on the Inside" (2005)
- "Building on Quicksand" (2009)
- "Immutability Changes Everything" (2015)
- "Standing on Distributed Shoulders of Giants" (2016)
- 《Life Beyond Distributed Transactions: An Apostate's Opinion》(2007, 2016)
- 《Data on the Outside vs Data on the Inside》(2005)
- 《Building on Quicksand》(2009)
- 《Immutability Changes Everything》(2015)
- 《Standing on Distributed Shoulders of Giants》(2016)