Event-Driven Architecture with Modern Patterns
This skill provides comprehensive patterns for implementing event-driven microservices using modern messaging systems, distributed runtimes, and cloud-native patterns. It's designed to be framework-agnostic and applicable to any domain requiring event-driven capabilities.
When to Use This Skill
Use this skill when you need to:
- Build event-driven microservices architecture
- Implement pub/sub patterns with Kafka, RabbitMQ, or cloud services
- Use Dapr for distributed application patterns
- Implement real-time notifications and workflows
- Build recurring task and reminder systems
- Create audit trails and activity logs
- Implement distributed state management
- Build serverless event workflows
- Handle event sourcing and CQRS patterns
1. Core Event Architecture
Base Event Schema
# events/core.py
from pydantic import BaseModel, Field, validator
from datetime import datetime
from typing import Optional, Dict, Any, Union, List
from enum import Enum
from abc import ABC, abstractmethod
import uuid
import json
class EventVersion(str, Enum):
"""Event versioning support"""
V1_0 = "1.0"
V1_1 = "1.1"
V2_0 = "2.0"
class EventPriority(str, Enum):
"""Event priority levels"""
LOW = "low"
NORMAL = "normal"
HIGH = "high"
CRITICAL = "critical"
class BaseEvent(BaseModel, ABC):
"""Base event schema with common fields"""
# Core identification
event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
event_type: str = Field(..., description="Type of the event")
event_version: EventVersion = Field(default=EventVersion.V1_0)
# Metadata
timestamp: datetime = Field(default_factory=datetime.utcnow)
correlation_id: Optional[str] = None
causation_id: Optional[str] = None # Event that caused this one
source: str = Field(..., description="Source service/identifier")
# Context
user_id: Optional[str] = None
tenant_id: Optional[str] = None # Multi-tenant support
session_id: Optional[str] = None
# Processing metadata
priority: EventPriority = Field(default=EventPriority.NORMAL)
retry_count: int = Field(default=0)
max_retries: int = Field(default=3)
delay_until: Optional[datetime] = None # For delayed processing
# Schema versioning
schema_version: str = Field(default="1.0")
class Config:
# Allow additional fields for extensibility
extra = "allow"
# Use enum values
use_enum_values = True
@validator('event_type')
def validate_event_type(cls, v):
"""Validate event type format"""
if not v or '.' not in v:
raise ValueError('event_type must be in format: domain.event_name')
return v.lower()
@validator('correlation_id')
def validate_correlation_id(cls, v, values):
"""Set correlation_id from causation_id if not provided"""
if not v and 'causation_id' in values:
return values['causation_id']
return v
def to_dict(self) -> Dict[str, Any]:
"""Convert event to dictionary for serialization"""
return self.model_dump()
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'BaseEvent':
"""Create event from dictionary"""
return cls(**data)
def with_context(self, **context) -> 'BaseEvent':
"""Add context to event"""
for key, value in context.items():
if hasattr(self, key):
setattr(self, key, value)
return self
class DomainEvent(BaseEvent):
"""Domain-specific event"""
aggregate_id: str = Field(..., description="Aggregate root ID")
aggregate_type: str = Field(..., description="Aggregate type")
event_data: Dict[str, Any] = Field(default_factory=dict)
@validator('aggregate_type')
def validate_aggregate_type(cls, v):
"""Validate aggregate type"""
if not v:
raise ValueError('aggregate_type is required')
return v.lower()
class IntegrationEvent(BaseEvent):
"""Integration event for cross-service communication"""
target_services: List[str] = Field(default_factory=list)
routing_key: Optional[str] = None
message_format: str = Field(default="json")
@validator('routing_key')
def validate_routing_key(cls, v, values):
"""Generate routing key from event_type if not provided"""
if not v and 'event_type' in values:
return values['event_type'].replace('.', '/')
return v
class CommandEvent(BaseEvent):
"""Command event representing an intent"""
command_type: str = Field(..., description="Type of command")
command_data: Dict[str, Any] = Field(default_factory=dict)
expected_version: Optional[int] = None # For optimistic concurrency
@validator('command_type')
def validate_command_type(cls, v):
"""Validate command type"""
if not v.endswith('.command'):
v = f"{v}.command"
return v.lower()
class QueryEvent(BaseEvent):
"""Query event for data retrieval"""
query_type: str = Field(..., description="Type of query")
query_params: Dict[str, Any] = Field(default_factory=dict)
result_topic: Optional[str] = None # Where to send results
@validator('query_type')
def validate_query_type(cls, v):
"""Validate query type"""
if not v.endswith('.query'):
v = f"{v}.query"
return v.lower()
Event Store Pattern
# events/store.py
import asyncio
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any, AsyncIterator
from datetime import datetime, timedelta
import json
import uuid
class EventStore(ABC):
"""Abstract event store interface"""
@abstractmethod
async def save_event(self, event: BaseEvent, stream_id: str) -> None:
"""Save event to a stream"""
pass
@abstractmethod
async def get_events(
self,
stream_id: str,
from_version: Optional[int] = None,
to_version: Optional[int] = None,
limit: Optional[int] = None
) -> AsyncIterator[BaseEvent]:
"""Get events from a stream"""
pass
@abstractmethod
async def get_event_by_id(self, event_id: str) -> Optional[BaseEvent]:
"""Get a specific event by ID"""
pass
@abstractmethod
async def get_events_by_type(
self,
event_type: str,
from_timestamp: Optional[datetime] = None,
to_timestamp: Optional[datetime] = None
) -> AsyncIterator[BaseEvent]:
"""Get events by type"""
pass
@abstractmethod
async def get_aggregate_snapshot(
self,
aggregate_id: str,
aggregate_type: str
) -> Optional[Dict[str, Any]]:
"""Get aggregate snapshot"""
pass
@abstractmethod
async def save_aggregate_snapshot(
self,
aggregate_id: str,
aggregate_type: str,
data: Dict[str, Any],
version: int
) -> None:
"""Save aggregate snapshot"""
pass
class InMemoryEventStore(EventStore):
"""In-memory event store for testing and development"""
def __init__(self):
self._events: Dict[str, List[BaseEvent]] = {}
self._snapshots: Dict[str, Dict[str, Any]] = {}
self._type_index: Dict[str, List[str]] = {}
async def save_event(self, event: BaseEvent, stream_id: str) -> None:
"""Save event to memory"""
if stream_id not in self._events:
self._events[stream_id] = []
self._events[stream_id].append(event)
# Update type index
if event.event_type not in self._type_index:
self._type_index[event.event_type] = []
self._type_index[event.event_type].append(event.event_id)
async def get_events(
self,
stream_id: str,
from_version: Optional[int] = None,
to_version: Optional[int] = None,
limit: Optional[int] = None
) -> AsyncIterator[BaseEvent]:
"""Get events from memory"""
events = self._events.get(stream_id, [])
# Apply filters
if from_version is not None:
events = events[from_version:]
if to_version is not None:
events = events[:to_version]
if limit is not None:
events = events[:limit]
for event in events:
yield event
async def get_event_by_id(self, event_id: str) -> Optional[BaseEvent]:
"""Get event by ID from memory"""
for events in self._events.values():
for event in events:
if event.event_id == event_id:
return event
return None
async def get_events_by_type(
self,
event_type: str,
from_timestamp: Optional[datetime] = None,
to_timestamp: Optional[datetime] = None
) -> AsyncIterator[BaseEvent]:
"""Get events by type from memory"""
event_ids = self._type_index.get(event_type, [])
for event_id in event_ids:
event = await self.get_event_by_id(event_id)
if event:
# Apply time filters
if from_timestamp and event.timestamp < from_timestamp:
continue
if to_timestamp and event.timestamp > to_timestamp:
continue
yield event
async def get_aggregate_snapshot(
self,
aggregate_id: str,
aggregate_type: str
) -> Optional[Dict[str, Any]]:
"""Get snapshot from memory"""
snapshot_key = f"{aggregate_type}:{aggregate_id}"
return self._snapshots.get(snapshot_key)
async def save_aggregate_snapshot(
self,
aggregate_id: str,
aggregate_type: str,
data: Dict[str, Any],
version: int
) -> None:
"""Save snapshot to memory"""
snapshot_key = f"{aggregate_type}:{aggregate_id}"
self._snapshots[snapshot_key] = {
"data": data,
"version": version,
"timestamp": datetime.utcnow()
}
# PostgreSQL Event Store Implementation
class PostgreSQLEventStore(EventStore):
"""PostgreSQL-based event store"""
def __init__(self, db_pool):
self.db_pool = db_pool
async def initialize_schema(self):
"""Initialize database schema"""
async with self.db_pool.acquire() as conn:
await conn.execute("""
CREATE TABLE IF NOT EXISTS event_store (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
stream_id VARCHAR(255) NOT NULL,
stream_version INTEGER NOT NULL,
event_id VARCHAR(255) UNIQUE NOT NULL,
event_type VARCHAR(255) NOT NULL,
event_data JSONB NOT NULL,
metadata JSONB,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
INDEX (stream_id, stream_version),
INDEX (event_type),
INDEX (timestamp)
);
CREATE TABLE IF NOT EXISTS snapshots (
aggregate_id VARCHAR(255) NOT NULL,
aggregate_type VARCHAR(255) NOT NULL,
data JSONB NOT NULL,
version INTEGER NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY (aggregate_type, aggregate_id)
);
""")
async def save_event(self, event: BaseEvent, stream_id: str) -> None:
"""Save event to PostgreSQL"""
async with self.db_pool.acquire() as conn:
# Get next version
result = await conn.fetchval(
"SELECT COALESCE(MAX(stream_version), 0) + 1 FROM event_store WHERE stream_id = $1",
stream_id
)
await conn.execute(
"""
INSERT INTO event_store (
stream_id, stream_version, event_id, event_type,
event_data, metadata, timestamp
) VALUES ($1, $2, $3, $4, $5, $6, $7)
""",
stream_id,
result,
event.event_id,
event.event_type,
event.to_dict(),
{
"correlation_id": event.correlation_id,
"causation_id": event.causation_id,
"source": event.source,
"user_id": event.user_id,
"tenant_id": event.tenant_id
},
event.timestamp
)
async def get_events(
self,
stream_id: str,
from_version: Optional[int] = None,
to_version: Optional[int] = None,
limit: Optional[int] = None
) -> AsyncIterator[BaseEvent]:
"""Get events from PostgreSQL"""
query = "SELECT event_data FROM event_store WHERE stream_id = $1"
params = [stream_id]
if from_version is not None:
query += " AND stream_version >= $2"
params.append(from_version)
version_offset = 2
else:
version_offset = 1
if to_version is not None:
query += f" AND stream_version <= ${version_offset + 1}"
params.append(to_version)
version_offset += 1
query += " ORDER BY stream_version"
if limit is not None:
query += f" LIMIT ${version_offset + 1}"
params.append(limit)
async with self.db_pool.acquire() as conn:
async for row in conn.cursor(query, *params):
yield BaseEvent.from_dict(row["event_data"])
async def get_aggregate_snapshot(
self,
aggregate_id: str,
aggregate_type: str
) -> Optional[Dict[str, Any]]:
"""Get snapshot from PostgreSQL"""
async with self.db_pool.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT data, version FROM snapshots
WHERE aggregate_id = $1 AND aggregate_type = $2
""",
aggregate_id,
aggregate_type
)
if row:
return {
"data": row["data"],
"version": row["version"]
}
return None
async def save_aggregate_snapshot(
self,
aggregate_id: str,
aggregate_type: str,
data: Dict[str, Any],
version: int
) -> None:
"""Save snapshot to PostgreSQL"""
async with self.db_pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO snapshots (aggregate_id, aggregate_type, data, version, timestamp)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (aggregate_type, aggregate_id)
DO UPDATE SET data = $3, version = $4, timestamp = NOW()
""",
aggregate_id,
aggregate_type,
data,
version
)
2. Message Broker Abstraction
Generic Message Broker Interface
# messaging/broker.py
import asyncio
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Callable, List, AsyncIterator
from dataclasses import dataclass
from enum import Enum
import json
import logging
logger = logging.getLogger(__name__)
class MessageBrokerType(str, Enum):
"""Supported message broker types"""
KAFKA = "kafka"
RABBITMQ = "rabbitmq"
REDIS = "redis"
AWS_SQS = "aws_sqs"
AZURE_SB = "azure_service_bus"
GCP_PUBSUB = "gcp_pubsub"
NATS = "nats"
EMQX = "emqx"
@dataclass
class Message:
"""Generic message representation"""
topic: str
key: Optional[str]
value: Any
headers: Optional[Dict[str, str]] = None
partition: Optional[int] = None
timestamp: Optional[int] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert message to dictionary"""
return {
"topic": self.topic,
"key": self.key,
"value": self.value,
"headers": self.headers or {},
"partition": self.partition,
"timestamp": self.timestamp
}
class MessageBroker(ABC):
"""Abstract message broker interface"""
@abstractmethod
async def connect(self) -> None:
"""Establish connection to broker"""
pass
@abstractmethod
async def disconnect(self) -> None:
"""Close connection to broker"""
pass
@abstractmethod
async def publish(self, message: Message) -> None:
"""Publish a message"""
pass
@abstractmethod
async def publish_batch(self, messages: List[Message]) -> None:
"""Publish multiple messages"""
pass
@abstractmethod
async def subscribe(
self,
topic: str,
handler: Callable[[Message], None],
group_id: Optional[str] = None
) -> None:
"""Subscribe to a topic"""
pass
@abstractmethod
async def create_topic(self, topic: str, **kwargs) -> None:
"""Create a topic if it doesn't exist"""
pass
class KafkaBroker(MessageBroker):
"""Kafka message broker implementation"""
def __init__(
self,
bootstrap_servers: str,
producer_config: Optional[Dict[str, Any]] = None,
consumer_config: Optional[Dict[str, Any]] = None
):
self.bootstrap_servers = bootstrap_servers
self.producer_config = producer_config or {}
self.consumer_config = consumer_config or {}
self.producer = None
self.consumers: List[Any] = []
# Default configurations
self.producer_config.update({
'bootstrap_servers': bootstrap_servers,
'value_serializer': lambda v: json.dumps(v).encode('utf-8'),
'key_serializer': lambda k: k.encode('utf-8') if k else None,
'acks': 'all',
'retries': 3,
'retry_backoff_ms': 100
})
self.consumer_config.update({
'bootstrap_servers': bootstrap_servers,
'value_deserializer': lambda v: json.loads(v.decode('utf-8')),
'key_deserializer': lambda k: k.decode('utf-8') if k else None,
'auto_offset_reset': 'earliest',
'enable_auto_commit': True
})
async def connect(self) -> None:
"""Connect to Kafka"""
from aiokafka import AIOKafkaProducer
self.producer = AIOKafkaProducer(**self.producer_config)
await self.producer.start()
logger.info("Connected to Kafka broker")
async def disconnect(self) -> None:
"""Disconnect from Kafka"""
if self.producer:
await self.producer.stop()
for consumer in self.consumers:
await consumer.stop()
logger.info("Disconnected from Kafka broker")
async def publish(self, message: Message) -> None:
"""Publish message to Kafka"""
if not self.producer:
raise RuntimeError("Producer not initialized")
await self.producer.send_and_wait(
topic=message.topic,
value=message.value,
key=message.key,
headers=message.headers,
partition=message.partition
)
async def publish_batch(self, messages: List[Message]) -> None:
"""Publish batch of messages to Kafka"""
if not self.producer:
raise RuntimeError("Producer not initialized")
# Group messages by topic
topics = {}
for msg in messages:
if msg.topic not in topics:
topics[msg.topic] = []
topics[msg.topic].append(msg)
# Send batched messages
for topic, topic_messages in topics.items():
for msg in topic_messages:
await self.producer.send_and_wait(
topic=topic,
value=msg.value,
key=msg.key,
headers=msg.headers,
partition=msg.partition
)
async def subscribe(
self,
topic: str,
handler: Callable[[Message], None],
group_id: Optional[str] = None
) -> None:
"""Subscribe to Kafka topic"""
from aiokafka import AIOKafkaConsumer
consumer_config = self.consumer_config.copy()
if group_id:
consumer_config['group_id'] = group_id
consumer = AIOKafkaConsumer(topic, **consumer_config)
await consumer.start()
self.consumers.append(consumer)
# Start consuming task
asyncio.create_task(self._consume_messages(consumer, handler))
async def _consume_messages(
self,
consumer: Any,
handler: Callable[[Message], None]
) -> None:
"""Consume messages from Kafka"""
try:
async for msg in consumer:
message = Message(
topic=msg.topic,
key=msg.key,
value=msg.value,
headers=dict(msg.headers) if msg.headers else {},
partition=msg.partition,
timestamp=msg.timestamp
)
try:
await self._handle_message(handler, message)
except Exception as e:
logger.error(f"Error handling message: {e}", exc_info=True)
except Exception as e:
logger.error(f"Consumer error: {e}", exc_info=True)
async def _handle_message(
self,
handler: Callable[[Message], None],
message: Message
) -> None:
"""Handle a single message"""
if asyncio.iscoroutinefunction(handler):
await handler(message)
else:
# Run sync handler in thread pool
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, handler, message)
async def create_topic(self, topic: str, **kwargs) -> None:
"""Create Kafka topic"""
from aiokafka.admin import AIOKafkaAdminClient, NewTopic
admin_client = AIOKafkaAdminClient(
bootstrap_servers=self.bootstrap_servers
)
try:
await admin_client.start()
topic_metadata = NewTopic(
name=topic,
num_partitions=kwargs.get('num_partitions', 3),
replication_factor=kwargs.get('replication_factor', 1)
)
await admin_client.create_topics([topic_metadata])
logger.info(f"Created topic: {topic}")
except Exception as e:
logger.warning(f"Failed to create topic {topic}: {e}")
finally:
await admin_client.close()
class RedisBroker(MessageBroker):
"""Redis-based message broker using streams"""
def __init__(self, redis_url: str):
self.redis_url = redis_url
self.redis = None
self.consumer_groups: Dict[str, str] = {}
async def connect(self) -> None:
"""Connect to Redis"""
import aioredis
self.redis = aioredis.from_url(self.redis_url)
await self.redis.ping()
logger.info("Connected to Redis broker")
async def disconnect(self) -> None:
"""Disconnect from Redis"""
if self.redis:
await self.redis.close()
async def publish(self, message: Message) -> None:
"""Publish message to Redis stream"""
if not self.redis:
raise RuntimeError("Redis not initialized")
message_data = {
'key': message.key,
'value': json.dumps(message.value),
'headers': json.dumps(message.headers or {}),
'timestamp': message.timestamp or int(time.time() * 1000)
}
await self.redis.xadd(message.topic, message_data)
async def publish_batch(self, messages: List[Message]) -> None:
"""Publish batch to Redis streams"""
# Redis doesn't have native batch xadd, so we use pipeline
pipe = self.redis.pipeline()
for message in messages:
message_data = {
'key': message.key,
'value': json.dumps(message.value),
'headers': json.dumps(message.headers or {}),
'timestamp': message.timestamp or int(time.time() * 1000)
}
pipe.xadd(message.topic, message_data)
await pipe.execute()
async def subscribe(
self,
topic: str,
handler: Callable[[Message], None],
group_id: Optional[str] = None
) -> None:
"""Subscribe to Redis stream"""
if group_id:
self.consumer_groups[topic] = group_id
try:
await self.redis.xgroup_create(topic, group_id, id='0', mkstream=True)
except Exception:
pass # Group might already exist
# Start consuming task
asyncio.create_task(self._consume_stream(topic, handler, group_id))
async def _consume_stream(
self,
topic: str,
handler: Callable[[Message], None],
group_id: Optional[str]
) -> None:
"""Consume messages from Redis stream"""
while True:
try:
if group_id:
# Consumer group mode
messages = await self.redis.xreadgroup(
group_id,
'consumer',
{topic: '>'},
count=10,
block=1000
)
else:
# Independent consumer mode
messages = await self.redis.xread(
{topic: '$'},
count=10,
block=1000
)
for stream, msgs in messages:
for msg_id, fields in msgs:
message = Message(
topic=topic,
key=fields.get('key'),
value=json.loads(fields['value']),
headers=json.loads(fields.get('headers', '{}')),
timestamp=int(fields.get('timestamp', 0))
)
try:
await self._handle_message(handler, message)
if group_id:
await self.redis.xack(topic, group_id, msg_id)
except Exception as e:
logger.error(f"Error handling message: {e}")
except Exception as e:
logger.error(f"Stream consumer error: {e}")
await asyncio.sleep(1)
class MessageBrokerFactory:
"""Factory for creating message brokers"""
@staticmethod
def create(broker_type: MessageBrokerType, **kwargs) -> MessageBroker:
"""Create a message broker instance"""
if broker_type == MessageBrokerType.KAFKA:
return KafkaBroker(
bootstrap_servers=kwargs['bootstrap_servers'],
producer_config=kwargs.get('producer_config'),
consumer_config=kwargs.get('consumer_config')
)
elif broker_type == MessageBrokerType.REDIS:
return RedisBroker(redis_url=kwargs['redis_url'])
else:
raise ValueError(f"Unsupported broker type: {broker_type}")
3. Event Processing Patterns
Saga Pattern Implementation
# patterns/saga.py
import asyncio
from typing import Dict, List, Optional, Any, Callable
from enum import Enum
from dataclasses import dataclass, field
import uuid
import logging
logger = logging.getLogger(__name__)
class SagaStatus(str, Enum):
"""Saga status enumeration"""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
COMPENSATING = "compensating"
COMPENSATED = "compensated"
FAILED = "failed"
@dataclass
class SagaStep:
"""Single step in a saga"""
step_id: str = field(default_factory=lambda: str(uuid.uuid4()))
action: Optional[Callable] = None
compensation: Optional[Callable] = None
params: Dict[str, Any] = field(default_factory=dict)
retry_count: int = 0
max_retries: int = 3
timeout: float = 30.0
completed: bool = False
result: Any = None
class Saga:
"""Saga orchestrator for distributed transactions"""
def __init__(self, saga_id: Optional[str] = None):
self.saga_id = saga_id or str(uuid.uuid4())
self.status = SagaStatus.PENDING
self.steps: List[SagaStep] = []
self.completed_steps: List[SagaStep] = []
self.current_step_index = 0
self.error: Optional[Exception] = None
self.created_at = datetime.utcnow()
self.updated_at = datetime.utcnow()
def add_step(
self,
action: Callable,
compensation: Callable,
params: Dict[str, Any] = None,
max_retries: int = 3,
timeout: float = 30.0
) -> 'Saga':
"""Add a step to the saga"""
step = SagaStep(
action=action,
compensation=compensation,
params=params or {},
max_retries=max_retries,
timeout=timeout
)
self.steps.append(step)
return self
async def execute(self) -> bool:
"""Execute the saga"""
try:
self.status = SagaStatus.RUNNING
self.updated_at = datetime.utcnow()
logger.info(f"Starting saga {self.saga_id} with {len(self.steps)} steps")
# Execute all steps
for i, step in enumerate(self.steps):
self.current_step_index = i
if not await self._execute_step(step):
# Step failed, start compensation
await self._compensate()
return False
self.completed_steps.append(step)
# All steps completed successfully
self.status = SagaStatus.COMPLETED
self.updated_at = datetime.utcnow()
logger.info(f"Saga {self.saga_id} completed successfully")
return True
except Exception as e:
self.error = e
logger.error(f"Saga {self.saga_id} failed: {e}")
await self._compensate()
return False
async def _execute_step(self, step: SagaStep) -> bool:
"""Execute a single step"""
logger.info(f"Executing step {step.step_id}")
while step.retry_count <= step.max_retries:
try:
# Execute with timeout
step.result = await asyncio.wait_for(
self._run_action(step.action, step.params),
timeout=step.timeout
)
step.completed = True
logger.info(f"Step {step.step_id} completed successfully")
return True
except asyncio.TimeoutError:
step.retry_count += 1
logger.warning(f"Step {step.step_id} timed out, retry {step.retry_count}")
await asyncio.sleep(2 ** step.retry_count) # Exponential backoff
except Exception as e:
step.retry_count += 1
logger.error(f"Step {step.step_id} failed: {e}")
if step.retry_count > step.max_retries:
logger.error(f"Step {step.step_id} failed after {step.max_retries} retries")
self.error = e
return False
await asyncio.sleep(2 ** step.retry_count) # Exponential backoff
return False
async def _run_action(self, action: Callable, params: Dict[str, Any]) -> Any:
"""Run action with parameters"""
if asyncio.iscoroutinefunction(action):
return await action(**params)
else:
# Run sync action in thread pool
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, action, **params)
async def _compensate(self) -> None:
"""Compensate completed steps"""
self.status = SagaStatus.COMPENSATING
self.updated_at = datetime.utcnow()
logger.info(f"Starting compensation for saga {self.saga_id}")
# Compensate in reverse order
for step in reversed(self.completed_steps):
try:
await self._compensate_step(step)
except Exception as e:
logger.error(f"Compensation failed for step {step.step_id}: {e}")
# Continue with other compensations
self.status = SagaStatus.COMPENSATED if self.error else SagaStatus.FAILED
self.updated_at = datetime.utcnow()
status_str = "compensated" if self.error else "failed"
logger.info(f"Saga {self.saga_id} {status_str}")
async def _compensate_step(self, step: SagaStep) -> None:
"""Compensate a single step"""
logger.info(f"Compensating step {step.step_id}")
compensation_params = step.params.copy()
compensation_params['result'] = step.result
if step.compensation:
if asyncio.iscoroutinefunction(step.compensation):
await step.compensation(**compensation_params)
else:
loop = asyncio.get_event_loop()
await loop.run_in_executor(
None,
step.compensation,
**compensation_params
)
class SagaManager:
"""Manages multiple sagas"""
def __init__(self, event_store: EventStore, message_broker: MessageBroker):
self.event_store = event_store
self.message_broker = message_broker
self.active_sagas: Dict[str, Saga] = {}
def create_saga(self, saga_id: Optional[str] = None) -> Saga:
"""Create a new saga"""
saga = Saga(saga_id)
self.active_sagas[saga.saga_id] = saga
return saga
async def execute_saga(self, saga: Saga) -> bool:
"""Execute a saga and persist its state"""
# Save saga started event
await self._save_saga_event(saga, "saga.started")
# Execute saga
result = await saga.execute()
# Save saga completed/failed event
event_type = "saga.completed" if result else "saga.failed"
await self._save_saga_event(saga, event_type)
# Remove from active sagas
if saga.saga_id in self.active_sagas:
del self.active_sagas[saga.saga_id]
return result
async def _save_saga_event(self, saga: Saga, event_type: str) -> None:
"""Save saga lifecycle event"""
event = DomainEvent(
event_type=event_type,
event_data={
"saga_id": saga.saga_id,
"status": saga.status.value,
"total_steps": len(saga.steps),
"completed_steps": len(saga.completed_steps),
"current_step": saga.current_step_index,
"error": str(saga.error) if saga.error else None
},
aggregate_id=saga.saga_id,
aggregate_type="saga",
source="saga_manager"
)
await self.event_store.save_event(event, f"saga-{saga.saga_id}")
# Also publish to message broker
message = Message(
topic="saga-events",
key=saga.saga_id,
value=event.to_dict()
)
await self.message_broker.publish(message)
CQRS Pattern Implementation
# patterns/cqrs.py
import asyncio
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Any, Type, TypeVar, Generic
from dataclasses import dataclass, field
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
T = TypeVar('T')
@dataclass
class CommandResponse:
"""Response from command execution"""
success: bool
message: Optional[str] = None
data: Optional[Dict[str, Any]] = None
errors: List[str] = field(default_factory=list)
class Command(ABC):
"""Base command class"""
def __init__(self, aggregate_id: str):
self.aggregate_id = aggregate_id
self.id = str(uuid.uuid4())
self.timestamp = datetime.utcnow()
class CommandHandler(ABC, Generic[T]):
"""Base command handler"""
@abstractmethod
async def handle(self, command: T) -> CommandResponse:
"""Handle the command"""
pass
class Query(ABC):
"""Base query class"""
def __init__(self):
self.id = str(uuid.uuid4())
self.timestamp = datetime.utcnow()
class QueryResult(ABC):
"""Base query result"""
pass
class QueryHandler(ABC, Generic[T]):
"""Base query handler"""
@abstractmethod
async def handle(self, query: T) -> QueryResult:
"""Handle the query"""
pass
class CommandBus:
"""Command bus for dispatching commands"""
def __init__(self):
self.handlers: Dict[Type[Command], CommandHandler] = {}
self.middleware: List[Callable] = []
def register(
self,
command_type: Type[Command],
handler: CommandHandler
) -> None:
"""Register a command handler"""
self.handlers[command_type] = handler
logger.info(f"Registered handler for {command_type.__name__}")
def add_middleware(self, middleware: Callable) -> None:
"""Add middleware to the command bus"""
self.middleware.append(middleware)
async def dispatch(self, command: Command) -> CommandResponse:
"""Dispatch a command to its handler"""
command_type = type(command)
if command_type not in self.handlers:
return CommandResponse(
success=False,
message=f"No handler registered for {command_type.__name__}"
)
handler = self.handlers[command_type]
# Apply middleware
for middleware in self.middleware:
command = await middleware(command)
try:
# Save command event
await self._save_command_event(command)
# Handle command
response = await handler.handle(command)
# Save response event
await self._save_response_event(command, response)
return response
except Exception as e:
logger.error(f"Error handling command {command_type.__name__}: {e}")
return CommandResponse(
success=False,
message=str(e),
errors=[str(e)]
)
async def _save_command_event(self, command: Command) -> None:
"""Save command event to event store"""
# Implementation would save to event store
pass
async def _save_response_event(
self,
command: Command,
response: CommandResponse
) -> None:
"""Save response event to event store"""
# Implementation would save to event store
pass
class QueryBus:
"""Query bus for dispatching queries"""
def __init__(self):
self.handlers: Dict[Type[Query], QueryHandler] = {}
self.middleware: List[Callable] = []
def register(
self,
query_type: Type[Query],
handler: QueryHandler
) -> None:
"""Register a query handler"""
self.handlers[query_type] = handler
logger.info(f"Registered handler for {query_type.__name__}")
def add_middleware(self, middleware: Callable) -> None:
"""Add middleware to the query bus"""
self.middleware.append(middleware)
async def dispatch(self, query: Query) -> QueryResult:
"""Dispatch a query to its handler"""
q
…(truncated)
1---2name: event-driven-architecture3description: Generic Event-Driven Architecture patterns with Kafka, Dapr, and modern messaging systems. Provides reusable patterns for building scalable, resilient event-driven microservices. Framework-agnostic implementation supporting multiple message brokers, state stores, and event patterns. Follows 2025 best practices for distributed systems.4license: MIT5---67# Event-Driven Architecture with Modern Patterns89This skill provides comprehensive patterns for implementing event-driven microservices using modern messaging systems, distributed runtimes, and cloud-native patterns. It's designed to be framework-agnostic and applicable to any domain requiring event-driven capabilities.1011## When to Use This Skill1213Use this skill when you need to:14- Build event-driven microservices architecture15- Implement pub/sub patterns with Kafka, RabbitMQ, or cloud services16- Use Dapr for distributed application patterns17- Implement real-time notifications and workflows18- Build recurring task and reminder systems19- Create audit trails and activity logs20- Implement distributed state management21- Build serverless event workflows22- Handle event sourcing and CQRS patterns2324## 1. Core Event Architecture2526### Base Event Schema2728```python29# events/core.py30from pydantic import BaseModel, Field, validator31from datetime import datetime32from typing import Optional, Dict, Any, Union, List33from enum import Enum34from abc import ABC, abstractmethod35import uuid36import json3738class EventVersion(str, Enum):39 """Event versioning support"""40 V1_0 = "1.0"41 V1_1 = "1.1"42 V2_0 = "2.0"4344class EventPriority(str, Enum):45 """Event priority levels"""46 LOW = "low"47 NORMAL = "normal"48 HIGH = "high"49 CRITICAL = "critical"5051class BaseEvent(BaseModel, ABC):52 """Base event schema with common fields"""5354 # Core identification55 event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))56 event_type: str = Field(..., description="Type of the event")57 event_version: EventVersion = Field(default=EventVersion.V1_0)5859 # Metadata60 timestamp: datetime = Field(default_factory=datetime.utcnow)61 correlation_id: Optional[str] = None62 causation_id: Optional[str] = None # Event that caused this one63 source: str = Field(..., description="Source service/identifier")6465 # Context66 user_id: Optional[str] = None67 tenant_id: Optional[str] = None # Multi-tenant support68 session_id: Optional[str] = None6970 # Processing metadata71 priority: EventPriority = Field(default=EventPriority.NORMAL)72 retry_count: int = Field(default=0)73 max_retries: int = Field(default=3)74 delay_until: Optional[datetime] = None # For delayed processing7576 # Schema versioning77 schema_version: str = Field(default="1.0")7879 class Config:80 # Allow additional fields for extensibility81 extra = "allow"82 # Use enum values83 use_enum_values = True8485 @validator('event_type')86 def validate_event_type(cls, v):87 """Validate event type format"""88 if not v or '.' not in v:89 raise ValueError('event_type must be in format: domain.event_name')90 return v.lower()9192 @validator('correlation_id')93 def validate_correlation_id(cls, v, values):94 """Set correlation_id from causation_id if not provided"""95 if not v and 'causation_id' in values:96 return values['causation_id']97 return v9899 def to_dict(self) -> Dict[str, Any]:100 """Convert event to dictionary for serialization"""101 return self.model_dump()102103 @classmethod104 def from_dict(cls, data: Dict[str, Any]) -> 'BaseEvent':105 """Create event from dictionary"""106 return cls(**data)107108 def with_context(self, **context) -> 'BaseEvent':109 """Add context to event"""110 for key, value in context.items():111 if hasattr(self, key):112 setattr(self, key, value)113 return self114115class DomainEvent(BaseEvent):116 """Domain-specific event"""117118 aggregate_id: str = Field(..., description="Aggregate root ID")119 aggregate_type: str = Field(..., description="Aggregate type")120 event_data: Dict[str, Any] = Field(default_factory=dict)121122 @validator('aggregate_type')123 def validate_aggregate_type(cls, v):124 """Validate aggregate type"""125 if not v:126 raise ValueError('aggregate_type is required')127 return v.lower()128129class IntegrationEvent(BaseEvent):130 """Integration event for cross-service communication"""131132 target_services: List[str] = Field(default_factory=list)133 routing_key: Optional[str] = None134 message_format: str = Field(default="json")135136 @validator('routing_key')137 def validate_routing_key(cls, v, values):138 """Generate routing key from event_type if not provided"""139 if not v and 'event_type' in values:140 return values['event_type'].replace('.', '/')141 return v142143class CommandEvent(BaseEvent):144 """Command event representing an intent"""145146 command_type: str = Field(..., description="Type of command")147 command_data: Dict[str, Any] = Field(default_factory=dict)148 expected_version: Optional[int] = None # For optimistic concurrency149150 @validator('command_type')151 def validate_command_type(cls, v):152 """Validate command type"""153 if not v.endswith('.command'):154 v = f"{v}.command"155 return v.lower()156157class QueryEvent(BaseEvent):158 """Query event for data retrieval"""159160 query_type: str = Field(..., description="Type of query")161 query_params: Dict[str, Any] = Field(default_factory=dict)162 result_topic: Optional[str] = None # Where to send results163164 @validator('query_type')165 def validate_query_type(cls, v):166 """Validate query type"""167 if not v.endswith('.query'):168 v = f"{v}.query"169 return v.lower()170```171172### Event Store Pattern173174```python175# events/store.py176import asyncio177from abc import ABC, abstractmethod178from typing import List, Optional, Dict, Any, AsyncIterator179from datetime import datetime, timedelta180import json181import uuid182183class EventStore(ABC):184 """Abstract event store interface"""185186 @abstractmethod187 async def save_event(self, event: BaseEvent, stream_id: str) -> None:188 """Save event to a stream"""189 pass190191 @abstractmethod192 async def get_events(193 self,194 stream_id: str,195 from_version: Optional[int] = None,196 to_version: Optional[int] = None,197 limit: Optional[int] = None198 ) -> AsyncIterator[BaseEvent]:199 """Get events from a stream"""200 pass201202 @abstractmethod203 async def get_event_by_id(self, event_id: str) -> Optional[BaseEvent]:204 """Get a specific event by ID"""205 pass206207 @abstractmethod208 async def get_events_by_type(209 self,210 event_type: str,211 from_timestamp: Optional[datetime] = None,212 to_timestamp: Optional[datetime] = None213 ) -> AsyncIterator[BaseEvent]:214 """Get events by type"""215 pass216217 @abstractmethod218 async def get_aggregate_snapshot(219 self,220 aggregate_id: str,221 aggregate_type: str222 ) -> Optional[Dict[str, Any]]:223 """Get aggregate snapshot"""224 pass225226 @abstractmethod227 async def save_aggregate_snapshot(228 self,229 aggregate_id: str,230 aggregate_type: str,231 data: Dict[str, Any],232 version: int233 ) -> None:234 """Save aggregate snapshot"""235 pass236237class InMemoryEventStore(EventStore):238 """In-memory event store for testing and development"""239240 def __init__(self):241 self._events: Dict[str, List[BaseEvent]] = {}242 self._snapshots: Dict[str, Dict[str, Any]] = {}243 self._type_index: Dict[str, List[str]] = {}244245 async def save_event(self, event: BaseEvent, stream_id: str) -> None:246 """Save event to memory"""247 if stream_id not in self._events:248 self._events[stream_id] = []249250 self._events[stream_id].append(event)251252 # Update type index253 if event.event_type not in self._type_index:254 self._type_index[event.event_type] = []255 self._type_index[event.event_type].append(event.event_id)256257 async def get_events(258 self,259 stream_id: str,260 from_version: Optional[int] = None,261 to_version: Optional[int] = None,262 limit: Optional[int] = None263 ) -> AsyncIterator[BaseEvent]:264 """Get events from memory"""265 events = self._events.get(stream_id, [])266267 # Apply filters268 if from_version is not None:269 events = events[from_version:]270 if to_version is not None:271 events = events[:to_version]272 if limit is not None:273 events = events[:limit]274275 for event in events:276 yield event277278 async def get_event_by_id(self, event_id: str) -> Optional[BaseEvent]:279 """Get event by ID from memory"""280 for events in self._events.values():281 for event in events:282 if event.event_id == event_id:283 return event284 return None285286 async def get_events_by_type(287 self,288 event_type: str,289 from_timestamp: Optional[datetime] = None,290 to_timestamp: Optional[datetime] = None291 ) -> AsyncIterator[BaseEvent]:292 """Get events by type from memory"""293 event_ids = self._type_index.get(event_type, [])294295 for event_id in event_ids:296 event = await self.get_event_by_id(event_id)297 if event:298 # Apply time filters299 if from_timestamp and event.timestamp < from_timestamp:300 continue301 if to_timestamp and event.timestamp > to_timestamp:302 continue303 yield event304305 async def get_aggregate_snapshot(306 self,307 aggregate_id: str,308 aggregate_type: str309 ) -> Optional[Dict[str, Any]]:310 """Get snapshot from memory"""311 snapshot_key = f"{aggregate_type}:{aggregate_id}"312 return self._snapshots.get(snapshot_key)313314 async def save_aggregate_snapshot(315 self,316 aggregate_id: str,317 aggregate_type: str,318 data: Dict[str, Any],319 version: int320 ) -> None:321 """Save snapshot to memory"""322 snapshot_key = f"{aggregate_type}:{aggregate_id}"323 self._snapshots[snapshot_key] = {324 "data": data,325 "version": version,326 "timestamp": datetime.utcnow()327 }328329# PostgreSQL Event Store Implementation330class PostgreSQLEventStore(EventStore):331 """PostgreSQL-based event store"""332333 def __init__(self, db_pool):334 self.db_pool = db_pool335336 async def initialize_schema(self):337 """Initialize database schema"""338 async with self.db_pool.acquire() as conn:339 await conn.execute("""340 CREATE TABLE IF NOT EXISTS event_store (341 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),342 stream_id VARCHAR(255) NOT NULL,343 stream_version INTEGER NOT NULL,344 event_id VARCHAR(255) UNIQUE NOT NULL,345 event_type VARCHAR(255) NOT NULL,346 event_data JSONB NOT NULL,347 metadata JSONB,348 timestamp TIMESTAMP WITH TIME ZONE NOT NULL,349 INDEX (stream_id, stream_version),350 INDEX (event_type),351 INDEX (timestamp)352 );353354 CREATE TABLE IF NOT EXISTS snapshots (355 aggregate_id VARCHAR(255) NOT NULL,356 aggregate_type VARCHAR(255) NOT NULL,357 data JSONB NOT NULL,358 version INTEGER NOT NULL,359 timestamp TIMESTAMP WITH TIME ZONE NOT NULL,360 PRIMARY KEY (aggregate_type, aggregate_id)361 );362 """)363364 async def save_event(self, event: BaseEvent, stream_id: str) -> None:365 """Save event to PostgreSQL"""366 async with self.db_pool.acquire() as conn:367 # Get next version368 result = await conn.fetchval(369 "SELECT COALESCE(MAX(stream_version), 0) + 1 FROM event_store WHERE stream_id = $1",370 stream_id371 )372373 await conn.execute(374 """375 INSERT INTO event_store (376 stream_id, stream_version, event_id, event_type,377 event_data, metadata, timestamp378 ) VALUES ($1, $2, $3, $4, $5, $6, $7)379 """,380 stream_id,381 result,382 event.event_id,383 event.event_type,384 event.to_dict(),385 {386 "correlation_id": event.correlation_id,387 "causation_id": event.causation_id,388 "source": event.source,389 "user_id": event.user_id,390 "tenant_id": event.tenant_id391 },392 event.timestamp393 )394395 async def get_events(396 self,397 stream_id: str,398 from_version: Optional[int] = None,399 to_version: Optional[int] = None,400 limit: Optional[int] = None401 ) -> AsyncIterator[BaseEvent]:402 """Get events from PostgreSQL"""403 query = "SELECT event_data FROM event_store WHERE stream_id = $1"404 params = [stream_id]405406 if from_version is not None:407 query += " AND stream_version >= $2"408 params.append(from_version)409 version_offset = 2410 else:411 version_offset = 1412413 if to_version is not None:414 query += f" AND stream_version <= ${version_offset + 1}"415 params.append(to_version)416 version_offset += 1417418 query += " ORDER BY stream_version"419420 if limit is not None:421 query += f" LIMIT ${version_offset + 1}"422 params.append(limit)423424 async with self.db_pool.acquire() as conn:425 async for row in conn.cursor(query, *params):426 yield BaseEvent.from_dict(row["event_data"])427428 async def get_aggregate_snapshot(429 self,430 aggregate_id: str,431 aggregate_type: str432 ) -> Optional[Dict[str, Any]]:433 """Get snapshot from PostgreSQL"""434 async with self.db_pool.acquire() as conn:435 row = await conn.fetchrow(436 """437 SELECT data, version FROM snapshots438 WHERE aggregate_id = $1 AND aggregate_type = $2439 """,440 aggregate_id,441 aggregate_type442 )443444 if row:445 return {446 "data": row["data"],447 "version": row["version"]448 }449 return None450451 async def save_aggregate_snapshot(452 self,453 aggregate_id: str,454 aggregate_type: str,455 data: Dict[str, Any],456 version: int457 ) -> None:458 """Save snapshot to PostgreSQL"""459 async with self.db_pool.acquire() as conn:460 await conn.execute(461 """462 INSERT INTO snapshots (aggregate_id, aggregate_type, data, version, timestamp)463 VALUES ($1, $2, $3, $4, NOW())464 ON CONFLICT (aggregate_type, aggregate_id)465 DO UPDATE SET data = $3, version = $4, timestamp = NOW()466 """,467 aggregate_id,468 aggregate_type,469 data,470 version471 )472```473474## 2. Message Broker Abstraction475476### Generic Message Broker Interface477478```python479# messaging/broker.py480import asyncio481from abc import ABC, abstractmethod482from typing import Any, Dict, Optional, Callable, List, AsyncIterator483from dataclasses import dataclass484from enum import Enum485import json486import logging487488logger = logging.getLogger(__name__)489490class MessageBrokerType(str, Enum):491 """Supported message broker types"""492 KAFKA = "kafka"493 RABBITMQ = "rabbitmq"494 REDIS = "redis"495 AWS_SQS = "aws_sqs"496 AZURE_SB = "azure_service_bus"497 GCP_PUBSUB = "gcp_pubsub"498 NATS = "nats"499 EMQX = "emqx"500501@dataclass502class Message:503 """Generic message representation"""504 topic: str505 key: Optional[str]506 value: Any507 headers: Optional[Dict[str, str]] = None508 partition: Optional[int] = None509 timestamp: Optional[int] = None510511 def to_dict(self) -> Dict[str, Any]:512 """Convert message to dictionary"""513 return {514 "topic": self.topic,515 "key": self.key,516 "value": self.value,517 "headers": self.headers or {},518 "partition": self.partition,519 "timestamp": self.timestamp520 }521522class MessageBroker(ABC):523 """Abstract message broker interface"""524525 @abstractmethod526 async def connect(self) -> None:527 """Establish connection to broker"""528 pass529530 @abstractmethod531 async def disconnect(self) -> None:532 """Close connection to broker"""533 pass534535 @abstractmethod536 async def publish(self, message: Message) -> None:537 """Publish a message"""538 pass539540 @abstractmethod541 async def publish_batch(self, messages: List[Message]) -> None:542 """Publish multiple messages"""543 pass544545 @abstractmethod546 async def subscribe(547 self,548 topic: str,549 handler: Callable[[Message], None],550 group_id: Optional[str] = None551 ) -> None:552 """Subscribe to a topic"""553 pass554555 @abstractmethod556 async def create_topic(self, topic: str, **kwargs) -> None:557 """Create a topic if it doesn't exist"""558 pass559560class KafkaBroker(MessageBroker):561 """Kafka message broker implementation"""562563 def __init__(564 self,565 bootstrap_servers: str,566 producer_config: Optional[Dict[str, Any]] = None,567 consumer_config: Optional[Dict[str, Any]] = None568 ):569 self.bootstrap_servers = bootstrap_servers570 self.producer_config = producer_config or {}571 self.consumer_config = consumer_config or {}572 self.producer = None573 self.consumers: List[Any] = []574575 # Default configurations576 self.producer_config.update({577 'bootstrap_servers': bootstrap_servers,578 'value_serializer': lambda v: json.dumps(v).encode('utf-8'),579 'key_serializer': lambda k: k.encode('utf-8') if k else None,580 'acks': 'all',581 'retries': 3,582 'retry_backoff_ms': 100583 })584585 self.consumer_config.update({586 'bootstrap_servers': bootstrap_servers,587 'value_deserializer': lambda v: json.loads(v.decode('utf-8')),588 'key_deserializer': lambda k: k.decode('utf-8') if k else None,589 'auto_offset_reset': 'earliest',590 'enable_auto_commit': True591 })592593 async def connect(self) -> None:594 """Connect to Kafka"""595 from aiokafka import AIOKafkaProducer596597 self.producer = AIOKafkaProducer(**self.producer_config)598 await self.producer.start()599 logger.info("Connected to Kafka broker")600601 async def disconnect(self) -> None:602 """Disconnect from Kafka"""603 if self.producer:604 await self.producer.stop()605606 for consumer in self.consumers:607 await consumer.stop()608609 logger.info("Disconnected from Kafka broker")610611 async def publish(self, message: Message) -> None:612 """Publish message to Kafka"""613 if not self.producer:614 raise RuntimeError("Producer not initialized")615616 await self.producer.send_and_wait(617 topic=message.topic,618 value=message.value,619 key=message.key,620 headers=message.headers,621 partition=message.partition622 )623624 async def publish_batch(self, messages: List[Message]) -> None:625 """Publish batch of messages to Kafka"""626 if not self.producer:627 raise RuntimeError("Producer not initialized")628629 # Group messages by topic630 topics = {}631 for msg in messages:632 if msg.topic not in topics:633 topics[msg.topic] = []634 topics[msg.topic].append(msg)635636 # Send batched messages637 for topic, topic_messages in topics.items():638 for msg in topic_messages:639 await self.producer.send_and_wait(640 topic=topic,641 value=msg.value,642 key=msg.key,643 headers=msg.headers,644 partition=msg.partition645 )646647 async def subscribe(648 self,649 topic: str,650 handler: Callable[[Message], None],651 group_id: Optional[str] = None652 ) -> None:653 """Subscribe to Kafka topic"""654 from aiokafka import AIOKafkaConsumer655656 consumer_config = self.consumer_config.copy()657 if group_id:658 consumer_config['group_id'] = group_id659660 consumer = AIOKafkaConsumer(topic, **consumer_config)661 await consumer.start()662 self.consumers.append(consumer)663664 # Start consuming task665 asyncio.create_task(self._consume_messages(consumer, handler))666667 async def _consume_messages(668 self,669 consumer: Any,670 handler: Callable[[Message], None]671 ) -> None:672 """Consume messages from Kafka"""673 try:674 async for msg in consumer:675 message = Message(676 topic=msg.topic,677 key=msg.key,678 value=msg.value,679 headers=dict(msg.headers) if msg.headers else {},680 partition=msg.partition,681 timestamp=msg.timestamp682 )683684 try:685 await self._handle_message(handler, message)686 except Exception as e:687 logger.error(f"Error handling message: {e}", exc_info=True)688 except Exception as e:689 logger.error(f"Consumer error: {e}", exc_info=True)690691 async def _handle_message(692 self,693 handler: Callable[[Message], None],694 message: Message695 ) -> None:696 """Handle a single message"""697 if asyncio.iscoroutinefunction(handler):698 await handler(message)699 else:700 # Run sync handler in thread pool701 loop = asyncio.get_event_loop()702 await loop.run_in_executor(None, handler, message)703704 async def create_topic(self, topic: str, **kwargs) -> None:705 """Create Kafka topic"""706 from aiokafka.admin import AIOKafkaAdminClient, NewTopic707708 admin_client = AIOKafkaAdminClient(709 bootstrap_servers=self.bootstrap_servers710 )711712 try:713 await admin_client.start()714715 topic_metadata = NewTopic(716 name=topic,717 num_partitions=kwargs.get('num_partitions', 3),718 replication_factor=kwargs.get('replication_factor', 1)719 )720721 await admin_client.create_topics([topic_metadata])722 logger.info(f"Created topic: {topic}")723 except Exception as e:724 logger.warning(f"Failed to create topic {topic}: {e}")725 finally:726 await admin_client.close()727728class RedisBroker(MessageBroker):729 """Redis-based message broker using streams"""730731 def __init__(self, redis_url: str):732 self.redis_url = redis_url733 self.redis = None734 self.consumer_groups: Dict[str, str] = {}735736 async def connect(self) -> None:737 """Connect to Redis"""738 import aioredis739740 self.redis = aioredis.from_url(self.redis_url)741 await self.redis.ping()742 logger.info("Connected to Redis broker")743744 async def disconnect(self) -> None:745 """Disconnect from Redis"""746 if self.redis:747 await self.redis.close()748749 async def publish(self, message: Message) -> None:750 """Publish message to Redis stream"""751 if not self.redis:752 raise RuntimeError("Redis not initialized")753754 message_data = {755 'key': message.key,756 'value': json.dumps(message.value),757 'headers': json.dumps(message.headers or {}),758 'timestamp': message.timestamp or int(time.time() * 1000)759 }760761 await self.redis.xadd(message.topic, message_data)762763 async def publish_batch(self, messages: List[Message]) -> None:764 """Publish batch to Redis streams"""765 # Redis doesn't have native batch xadd, so we use pipeline766 pipe = self.redis.pipeline()767768 for message in messages:769 message_data = {770 'key': message.key,771 'value': json.dumps(message.value),772 'headers': json.dumps(message.headers or {}),773 'timestamp': message.timestamp or int(time.time() * 1000)774 }775 pipe.xadd(message.topic, message_data)776777 await pipe.execute()778779 async def subscribe(780 self,781 topic: str,782 handler: Callable[[Message], None],783 group_id: Optional[str] = None784 ) -> None:785 """Subscribe to Redis stream"""786 if group_id:787 self.consumer_groups[topic] = group_id788 try:789 await self.redis.xgroup_create(topic, group_id, id='0', mkstream=True)790 except Exception:791 pass # Group might already exist792793 # Start consuming task794 asyncio.create_task(self._consume_stream(topic, handler, group_id))795796 async def _consume_stream(797 self,798 topic: str,799 handler: Callable[[Message], None],800 group_id: Optional[str]801 ) -> None:802 """Consume messages from Redis stream"""803 while True:804 try:805 if group_id:806 # Consumer group mode807 messages = await self.redis.xreadgroup(808 group_id,809 'consumer',810 {topic: '>'},811 count=10,812 block=1000813 )814 else:815 # Independent consumer mode816 messages = await self.redis.xread(817 {topic: '$'},818 count=10,819 block=1000820 )821822 for stream, msgs in messages:823 for msg_id, fields in msgs:824 message = Message(825 topic=topic,826 key=fields.get('key'),827 value=json.loads(fields['value']),828 headers=json.loads(fields.get('headers', '{}')),829 timestamp=int(fields.get('timestamp', 0))830 )831832 try:833 await self._handle_message(handler, message)834835 if group_id:836 await self.redis.xack(topic, group_id, msg_id)837 except Exception as e:838 logger.error(f"Error handling message: {e}")839 except Exception as e:840 logger.error(f"Stream consumer error: {e}")841 await asyncio.sleep(1)842843class MessageBrokerFactory:844 """Factory for creating message brokers"""845846 @staticmethod847 def create(broker_type: MessageBrokerType, **kwargs) -> MessageBroker:848 """Create a message broker instance"""849 if broker_type == MessageBrokerType.KAFKA:850 return KafkaBroker(851 bootstrap_servers=kwargs['bootstrap_servers'],852 producer_config=kwargs.get('producer_config'),853 consumer_config=kwargs.get('consumer_config')854 )855 elif broker_type == MessageBrokerType.REDIS:856 return RedisBroker(redis_url=kwargs['redis_url'])857 else:858 raise ValueError(f"Unsupported broker type: {broker_type}")859```860861## 3. Event Processing Patterns862863### Saga Pattern Implementation864865```python866# patterns/saga.py867import asyncio868from typing import Dict, List, Optional, Any, Callable869from enum import Enum870from dataclasses import dataclass, field871import uuid872import logging873874logger = logging.getLogger(__name__)875876class SagaStatus(str, Enum):877 """Saga status enumeration"""878 PENDING = "pending"879 RUNNING = "running"880 COMPLETED = "completed"881 COMPENSATING = "compensating"882 COMPENSATED = "compensated"883 FAILED = "failed"884885@dataclass886class SagaStep:887 """Single step in a saga"""888 step_id: str = field(default_factory=lambda: str(uuid.uuid4()))889 action: Optional[Callable] = None890 compensation: Optional[Callable] = None891 params: Dict[str, Any] = field(default_factory=dict)892 retry_count: int = 0893 max_retries: int = 3894 timeout: float = 30.0895 completed: bool = False896 result: Any = None897898class Saga:899 """Saga orchestrator for distributed transactions"""900901 def __init__(self, saga_id: Optional[str] = None):902 self.saga_id = saga_id or str(uuid.uuid4())903 self.status = SagaStatus.PENDING904 self.steps: List[SagaStep] = []905 self.completed_steps: List[SagaStep] = []906 self.current_step_index = 0907 self.error: Optional[Exception] = None908 self.created_at = datetime.utcnow()909 self.updated_at = datetime.utcnow()910911 def add_step(912 self,913 action: Callable,914 compensation: Callable,915 params: Dict[str, Any] = None,916 max_retries: int = 3,917 timeout: float = 30.0918 ) -> 'Saga':919 """Add a step to the saga"""920 step = SagaStep(921 action=action,922 compensation=compensation,923 params=params or {},924 max_retries=max_retries,925 timeout=timeout926 )927 self.steps.append(step)928 return self929930 async def execute(self) -> bool:931 """Execute the saga"""932 try:933 self.status = SagaStatus.RUNNING934 self.updated_at = datetime.utcnow()935936 logger.info(f"Starting saga {self.saga_id} with {len(self.steps)} steps")937938 # Execute all steps939 for i, step in enumerate(self.steps):940 self.current_step_index = i941942 if not await self._execute_step(step):943 # Step failed, start compensation944 await self._compensate()945 return False946947 self.completed_steps.append(step)948949 # All steps completed successfully950 self.status = SagaStatus.COMPLETED951 self.updated_at = datetime.utcnow()952 logger.info(f"Saga {self.saga_id} completed successfully")953 return True954955 except Exception as e:956 self.error = e957 logger.error(f"Saga {self.saga_id} failed: {e}")958 await self._compensate()959 return False960961 async def _execute_step(self, step: SagaStep) -> bool:962 """Execute a single step"""963 logger.info(f"Executing step {step.step_id}")964965 while step.retry_count <= step.max_retries:966 try:967 # Execute with timeout968 step.result = await asyncio.wait_for(969 self._run_action(step.action, step.params),970 timeout=step.timeout971 )972973 step.completed = True974 logger.info(f"Step {step.step_id} completed successfully")975 return True976977 except asyncio.TimeoutError:978 step.retry_count += 1979 logger.warning(f"Step {step.step_id} timed out, retry {step.retry_count}")980 await asyncio.sleep(2 ** step.retry_count) # Exponential backoff981982 except Exception as e:983 step.retry_count += 1984 logger.error(f"Step {step.step_id} failed: {e}")985986 if step.retry_count > step.max_retries:987 logger.error(f"Step {step.step_id} failed after {step.max_retries} retries")988 self.error = e989 return False990991 await asyncio.sleep(2 ** step.retry_count) # Exponential backoff992993 return False994995 async def _run_action(self, action: Callable, params: Dict[str, Any]) -> Any:996 """Run action with parameters"""997 if asyncio.iscoroutinefunction(action):998 return await action(**params)999 else:1000 # Run sync action in thread pool1001 loop = asyncio.get_event_loop()1002 return await loop.run_in_executor(None, action, **params)10031004 async def _compensate(self) -> None:1005 """Compensate completed steps"""1006 self.status = SagaStatus.COMPENSATING1007 self.updated_at = datetime.utcnow()10081009 logger.info(f"Starting compensation for saga {self.saga_id}")10101011 # Compensate in reverse order1012 for step in reversed(self.completed_steps):1013 try:1014 await self._compensate_step(step)1015 except Exception as e:1016 logger.error(f"Compensation failed for step {step.step_id}: {e}")1017 # Continue with other compensations10181019 self.status = SagaStatus.COMPENSATED if self.error else SagaStatus.FAILED1020 self.updated_at = datetime.utcnow()10211022 status_str = "compensated" if self.error else "failed"1023 logger.info(f"Saga {self.saga_id} {status_str}")10241025 async def _compensate_step(self, step: SagaStep) -> None:1026 """Compensate a single step"""1027 logger.info(f"Compensating step {step.step_id}")10281029 compensation_params = step.params.copy()1030 compensation_params['result'] = step.result10311032 if step.compensation:1033 if asyncio.iscoroutinefunction(step.compensation):1034 await step.compensation(**compensation_params)1035 else:1036 loop = asyncio.get_event_loop()1037 await loop.run_in_executor(1038 None,1039 step.compensation,1040 **compensation_params1041 )10421043class SagaManager:1044 """Manages multiple sagas"""10451046 def __init__(self, event_store: EventStore, message_broker: MessageBroker):1047 self.event_store = event_store1048 self.message_broker = message_broker1049 self.active_sagas: Dict[str, Saga] = {}10501051 def create_saga(self, saga_id: Optional[str] = None) -> Saga:1052 """Create a new saga"""1053 saga = Saga(saga_id)1054 self.active_sagas[saga.saga_id] = saga1055 return saga10561057 async def execute_saga(self, saga: Saga) -> bool:1058 """Execute a saga and persist its state"""1059 # Save saga started event1060 await self._save_saga_event(saga, "saga.started")10611062 # Execute saga1063 result = await saga.execute()10641065 # Save saga completed/failed event1066 event_type = "saga.completed" if result else "saga.failed"1067 await self._save_saga_event(saga, event_type)10681069 # Remove from active sagas1070 if saga.saga_id in self.active_sagas:1071 del self.active_sagas[saga.saga_id]10721073 return result10741075 async def _save_saga_event(self, saga: Saga, event_type: str) -> None:1076 """Save saga lifecycle event"""1077 event = DomainEvent(1078 event_type=event_type,1079 event_data={1080 "saga_id": saga.saga_id,1081 "status": saga.status.value,1082 "total_steps": len(saga.steps),1083 "completed_steps": len(saga.completed_steps),1084 "current_step": saga.current_step_index,1085 "error": str(saga.error) if saga.error else None1086 },1087 aggregate_id=saga.saga_id,1088 aggregate_type="saga",1089 source="saga_manager"1090 )10911092 await self.event_store.save_event(event, f"saga-{saga.saga_id}")10931094 # Also publish to message broker1095 message = Message(1096 topic="saga-events",1097 key=saga.saga_id,1098 value=event.to_dict()1099 )1100 await self.message_broker.publish(message)1101```11021103### CQRS Pattern Implementation11041105```python1106# patterns/cqrs.py1107import asyncio1108from abc import ABC, abstractmethod1109from typing import Dict, List, Optional, Any, Type, TypeVar, Generic1110from dataclasses import dataclass, field1111from datetime import datetime1112import logging11131114logger = logging.getLogger(__name__)11151116T = TypeVar('T')11171118@dataclass1119class CommandResponse:1120 """Response from command execution"""1121 success: bool1122 message: Optional[str] = None1123 data: Optional[Dict[str, Any]] = None1124 errors: List[str] = field(default_factory=list)11251126class Command(ABC):1127 """Base command class"""11281129 def __init__(self, aggregate_id: str):1130 self.aggregate_id = aggregate_id1131 self.id = str(uuid.uuid4())1132 self.timestamp = datetime.utcnow()11331134class CommandHandler(ABC, Generic[T]):1135 """Base command handler"""11361137 @abstractmethod1138 async def handle(self, command: T) -> CommandResponse:1139 """Handle the command"""1140 pass11411142class Query(ABC):1143 """Base query class"""11441145 def __init__(self):1146 self.id = str(uuid.uuid4())1147 self.timestamp = datetime.utcnow()11481149class QueryResult(ABC):1150 """Base query result"""1151 pass11521153class QueryHandler(ABC, Generic[T]):1154 """Base query handler"""11551156 @abstractmethod1157 async def handle(self, query: T) -> QueryResult:1158 """Handle the query"""1159 pass11601161class CommandBus:1162 """Command bus for dispatching commands"""11631164 def __init__(self):1165 self.handlers: Dict[Type[Command], CommandHandler] = {}1166 self.middleware: List[Callable] = []11671168 def register(1169 self,1170 command_type: Type[Command],1171 handler: CommandHandler1172 ) -> None:1173 """Register a command handler"""1174 self.handlers[command_type] = handler1175 logger.info(f"Registered handler for {command_type.__name__}")11761177 def add_middleware(self, middleware: Callable) -> None:1178 """Add middleware to the command bus"""1179 self.middleware.append(middleware)11801181 async def dispatch(self, command: Command) -> CommandResponse:1182 """Dispatch a command to its handler"""1183 command_type = type(command)11841185 if command_type not in self.handlers:1186 return CommandResponse(1187 success=False,1188 message=f"No handler registered for {command_type.__name__}"1189 )11901191 handler = self.handlers[command_type]11921193 # Apply middleware1194 for middleware in self.middleware:1195 command = await middleware(command)11961197 try:1198 # Save command event1199 await self._save_command_event(command)12001201 # Handle command1202 response = await handler.handle(command)12031204 # Save response event1205 await self._save_response_event(command, response)12061207 return response12081209 except Exception as e:1210 logger.error(f"Error handling command {command_type.__name__}: {e}")1211 return CommandResponse(1212 success=False,1213 message=str(e),1214 errors=[str(e)]1215 )12161217 async def _save_command_event(self, command: Command) -> None:1218 """Save command event to event store"""1219 # Implementation would save to event store1220 pass12211222 async def _save_response_event(1223 self,1224 command: Command,1225 response: CommandResponse1226 ) -> None:1227 """Save response event to event store"""1228 # Implementation would save to event store1229 pass12301231class QueryBus:1232 """Query bus for dispatching queries"""12331234 def __init__(self):1235 self.handlers: Dict[Type[Query], QueryHandler] = {}1236 self.middleware: List[Callable] = []12371238 def register(1239 self,1240 query_type: Type[Query],1241 handler: QueryHandler1242 ) -> None:1243 """Register a query handler"""1244 self.handlers[query_type] = handler1245 logger.info(f"Registered handler for {query_type.__name__}")12461247 def add_middleware(self, middleware: Callable) -> None:1248 """Add middleware to the query bus"""1249 self.middleware.append(middleware)12501251 async def dispatch(self, query: Query) -> QueryResult:1252 """Dispatch a query to its handler"""1253 query_type = type(query)12541255 if query_type not in self.handlers:1256 raise ValueError(f"No handler registered for {query_type.__name__}")12571258 handler = self.handlers[query_type]12591260 # Apply middleware1261 for middleware in self.middleware:1262 query = await middleware(query)12631264 # Handle query1265 return await handler.handle(query)12661267class EventProjector(ABC):1268 """Base event projector for building read models"""12691270 def __init__(self, event_store: EventStore):1271 self.event_store = event_store12721273 @abstractmethod1274 async def project(self, event: BaseEvent) -> None:1275 """Project an event to the read model"""1276 pass12771278 async def project_from_stream(1279 self,1280 stream_id: str,1281 from_version: Optional[int] = None1282 ) -> None:1283 """Project events from a stream"""1284 async for event in self.event_store.get_events(1285 stream_id,1286 from_version=from_version1287 ):1288 await self.project(event)12891290 async def project_by_type(1291 self,1292 event_type: str,1293 from_timestamp: Optional[datetime] = None1294 ) -> None:1295 """Project events by type"""1296 async for event in self.event_store.get_events_by_type(1297 event_type,1298 from_timestamp=from_timestamp1299 ):1300 await self.project(event)13011302# Example CQRS Implementation1303class CreateTaskCommand(Command):1304 """Create task command"""13051306 def __init__(self, aggregate_id: str, title: str, description: str = None):1307 super().__init__(aggregate_id)1308 self.title = title1309 self.description = description13101311class TaskCreatedEvent(DomainEvent):1312 """Task created event"""13131314 def __init__(self, aggregate_id: str, title: str, description: str = None):1315 super().__init__(1316 event_type="task.created",1317 aggregate_id=aggregate_id,1318 aggregate_type="task",1319 event_data={1320 "title": title,1321 "description": description1322 }1323 )13241325class TaskReadModel:1326 """Task read model for queries"""13271328 def __init__(self):1329 self.tasks: Dict[str, Dict[str, Any]] = {}13301331 async def get_task(self, task_id: str) -> Optional[Dict[str, Any]]:1332 """Get task from read model"""1333 return self.tasks.get(task_id)13341335 async def list_tasks(self, user_id: str = None) -> List[Dict[str, Any]]:1336 """List all tasks or user's tasks"""1337 tasks = list(self.tasks.values())1338 if user_id:1339 tasks = [t for t in tasks if t.get("user_id") == user_id]1340 return tasks13411342class TaskProjector(EventProjector):1343 """Task event projector"""13441345 def __init__(self, event_store: EventStore, read_model: TaskReadModel):1346 super().__init__(event_store)1347 self.read_model = read_model13481349 async def project(self, event: BaseEvent) -> None:1350 """Project task events to read model"""1351 if event.event_type == "task.created":1352 await self._project_task_created(event)1353 elif event.event_type == "task.updated":1354 await self._project_task_updated(event)1355 elif event.event_type == "task.deleted":1356 await self._project_task_deleted(event)13571358 async def _project_task_created(self, event: DomainEvent) -> None:1359 """Project task created event"""1360 self.read_model.tasks[event.aggregate_id] = {1361 "id": event.aggregate_id,1362 "title": event.event_data["title"],1363 "description": event.event_data.get("description"),1364 "status": "todo",1365 "created_at": event.timestamp,1366 "updated_at": event.timestamp1367 }13681369 async def _project_task_updated(self, event: DomainEvent) -> None:1370 """Project task updated event"""1371 if event.aggregate_id in self.read_model.tasks:1372 task = self.read_model.tasks[event.aggregate_id]1373 task.update(event.event_data)1374 task["updated_at"] = event.timestamp13751376 async def _project_task_deleted(self, event: DomainEvent) -> None:1377 """Project task deleted event"""1378 if event.aggregate_id in self.read_model.tasks:1379 del self.read_model.tasks[event.aggregate_id]1380```13811382## 4. Dapr Integration Patterns13831384### Generic Dapr Client13851386```python1387# dapr/client.py1388import asyncio1389import httpx1390from typing import Dict, Any, List, Optional1391import json1392import logging13931394logger = logging.getLogger(__name__)13951396class DaprClient:1397 """Generic Dapr client with comprehensive feature support"""13981399 def __init__(1400 self,1401 dapr_port: int = 3500,1402 dapr_host: str = "localhost",1403 timeout: float = 30.01404 ):1405 self.dapr_port = dapr_port1406 self.dapr_host = dapr_host1407 self.base_url = f"http://{dapr_host}:{dapr_port}/v1.0"1408 self.timeout = timeout14091410 # HTTP client for Dapr API calls1411 self.client = httpx.AsyncClient(timeout=timeout)14121413 async def close(self):1414 """Close the Dapr client"""1415 await self.client.aclose()14161417 # State Management1418 async def save_state(1419 self,1420 store_name: str,1421 key: str,1422 value: Any,1423 metadata: Optional[Dict[str, str]] = None,1424 options: Optional[Dict[str, Any]] = None1425 ) -> None:1426 """Save state to Dapr state store"""1427 url = f"{self.base_url}/state/{store_name}"14281429 state_item = {1430 "key": key,1431 "value": value1432 }14331434 if metadata:1435 state_item["metadata"] = metadata14361437 if options:1438 state_item["options"] = options14391440 response = await self.client.post(url, json=[state_item])1441 response.raise_for_status()14421443 async def get_state(1444 self,1445 store_name: str,1446 key: str,1447 metadata: Optional[Dict[str, str]] = None1448 ) -> Any:1449 """Get state from Dapr state store"""1450 url = f"{self.base_url}/state/{store_name}/{key}"14511452 params = {}1453 if metadata:1454 params["metadata"] = json.dumps(metadata)14551456 response = await self.client.get(url, params=params)1457 response.raise_for_status()14581459 if response.status_code == 204:1460 return None14611462 return response.json()14631464 async def delete_state(1465 self,1466 store_name: str,1467 key: str,1468 metadata: Optional[Dict[str, str]] = None1469 ) -> None:1470 """Delete state from Dapr state store"""1471 url = f"{self.base_url}/state/{store_name}/{key}"14721473 params = {}1474 if metadata:1475 params["metadata"] = json.dumps(metadata)14761477 response = await self.client.delete(url, params=params)1478 response.raise_for_status()14791480 async def get_bulk_state(1481 self,1482 store_name: str,1483 keys: List[str],1484 parallelism: int = 10,1485 metadata: Optional[Dict[str, str]] = None1486 ) -> Dict[str, Any]:1487 """Get multiple state items"""1488 url = f"{self.base_url}/state/{store_name}/bulk"14891490 request_data = {1491 "keys": keys,1492 "parallelism": parallelism1493 }14941495 if metadata:1496 request_data["metadata"] = metadata14971498 response = await self.client.post(url, json=request_data)1499 response.raise_for_status()15001501 return response.json()15021503 # Pub/Sub1504 async def publish_event(1505 self,1506 pubsub_name: str,1507 topic: str,1508 data: Any,1509 metadata: Optional[Dict[str, str]] = None1510 ) -> None:1511 """Publish event via Dapr pub/sub"""1512 url = f"{self.base_url}/publish/{pubsub_name}/{topic}"15131514 headers = {}1515 if metadata:1516 for key, value in metadata.items():1517 headers[f"metadata-{key}"] = value15181519 response = await self.client.post(1520 url,1521 content=json.dumps(data),1522 headers=headers1523 )1524 response.raise_for_status()15251526 # Service Invocation1527 async def invoke_service(1528 self,1529 app_id: str,1530 method_name: str,1531 data: Any = None,1532 http_verb: str = "POST",1533 metadata: Optional[Dict[str, str]] = None1534 ) -> Any:1535 """Invoke a service method"""1536 url = f"{self.base_url}/invoke/{app_id}/method/{method_name}"15371538 headers = {}1539 if metadata:1540 for key, value in metadata.items():1541 headers[f"metadata-{key}"] = value15421543 response = await self.client.request(1544 http_verb,1545 url,1546 content=json.dumps(data) if data else None,1547 headers=headers1548 )1549 response.raise_for_status()15501551 return response.json()15521553 # Bindings1554 async def invoke_binding(1555 self,1556 binding_name: str,1557 operation: str,1558 data: Any,1559 metadata: Optional[Dict[str, str]] = None1560 ) -> Any:1561 """Invoke an output binding"""1562 url = f"{self.base_url}/bindings/{binding_name}"15631564 request_data = {1565 "operation": operation,1566 "data": data1567 }15681569 if metadata:1570 request_data["metadata"] = metadata15711572 response = await self.client.post(url, json=request_data)1573 response.raise_for_status()15741575 return response.json()15761577 # Secrets1578 async def get_secret(1579 self,1580 secret_store_name: str,1581 key: str,1582 metadata: Optional[Dict[str, str]] = None1583 ) -> str:1584 """Get secret from Dapr secret store"""1585 url = f"{self.base_url}/secrets/{secret_store_name}/{key}"15861587 params = {}1588 if metadata:1589 params["metadata"] = json.dumps(metadata)15901591 response = await self.client.get(url, params=params)1592 response.raise_for_status()15931594 return response.json()[key]15951596 # Configuration1597 async def get_configuration(1598 self,1599 configuration_store_name: str,1600 keys: List[str],1601 metadata: Optional[Dict[str, str]] = None1602 ) -> Dict[str, Any]:1603 """Get configuration from Dapr configuration store"""1604 url = f"{self.base_url}/configuration/{configuration_store_name}"16051606 request_data = {"keys": keys}1607 if metadata:1608 request_data["metadata"] = metadata16091610 response = await self.client.post(url, json=request_data)1611 response.raise_for_status()16121613 return response.json()16141615 # Distributed Lock1616 async def acquire_lock(1617 self,1618 lock_store_name: str,1619 resource_id: str,1620 lock_owner: str,1621 expiry_in_seconds: int = 601622 ) -> bool:1623 """Acquire distributed lock"""1624 url = f"{self.base_url}/locks/{lock_store_name}/{resource_id}"16251626 request_data = {1627 "lockOwner": lock_owner,1628 "expiryInSeconds": expiry_in_seconds1629 }16301631 response = await self.client.post(url, json=request_data)1632 response.raise_for_status()16331634 return response.json().get("success", False)16351636 async def release_lock(1637 self,1638 lock_store_name: str,1639 resource_id: str,1640 lock_owner: str1641 ) -> bool:1642 """Release distributed lock"""1643 url = f"{self.base_url}/locks/{lock_store_name}/{resource_id}/{lock_owner}"16441645 response = await self.client.delete(url)1646 response.raise_for_status()16471648 return response.json().get("success", False)16491650 # Health Checks1651 async def health_check(self) -> bool:1652 """Check Dapr sidecar health"""1653 try:1654 url = f"{self.base_url}/healthz"1655 response = await self.client.get(url)1656 return response.status_code == 2001657 except Exception as e:1658 logger.error(f"Dapr health check failed: {e}")1659 return False16601661class DaprWorkflow:1662 """Dapr Workflow API client"""16631664 def __init__(self, dapr_client: DaprClient):1665 self.dapr_client = dapr_client16661667 async def start_workflow(1668 self,1669 workflow_name: str,1670 input_data: Any = None,1671 instance_id: Optional[str] = None1672 ) -> str:1673 """Start a workflow instance"""1674 url = f"{self.dapr_client.base_url}/workflows/{workflow_name}/start"16751676 request_data = {}1677 if input_data:1678 request_data["input"] = input_data1679 if instance_id:1680 request_data["workflowInstanceId"] = instance_id16811682 response = await self.dapr_client.client.post(url, json=request_data)1683 response.raise_for_status()16841685 return response.json().get("instanceId")16861687 async def get_workflow_state(1688 self,1689 instance_id: str1690 ) -> Dict[str, Any]:1691 """Get workflow state"""1692 url = f"{self.dapr_client.base_url}/workflows/{instance_id}"16931694 response = await self.dapr_client.client.get(url)1695 response.raise_for_status()16961697 return response.json()16981699 async def raise_workflow_event(1700 self,1701 instance_id: str,1702 event_name: str,1703 event_data: Any1704 ) -> None:1705 """Raise an event for a workflow"""1706 url = f"{self.dapr_client.base_url}/workflows/{instance_id}/raiseEvent/{event_name}"17071708 response = await self.dapr_client.client.post(1709 url,1710 content=json.dumps(event_data)1711 )1712 response.raise_for_status()17131714 async def terminate_workflow(1715 self,1716 instance_id: str1717 ) -> None:1718 """Terminate a workflow instance"""1719 url = f"{self.dapr_client.base_url}/workflows/{instance_id}/terminate"17201721 response = await self.dapr_client.client.post(url)1722 response.raise_for_status()17231724# Dapr Event Store Integration1725class DaprEventStore(EventStore):1726 """Dapr-based event store implementation"""17271728 def __init__(self, dapr_client: DaprClient, state_store_name: str = "eventstore"):1729 self.dapr_client = dapr_client1730 self.state_store_name = state_store_name17311732 async def save_event(self, event: BaseEvent, stream_id: str) -> None:1733 """Save event using Dapr state store"""1734 # Get stream events1735 stream_key = f"stream:{stream_id}"1736 events = await self.dapr_client.get_state(1737 self.state_store_name,1738 stream_key1739 ) or []17401741 # Add new event1742 events.append(event.to_dict())17431744 # Save back to state store1745 await self.dapr_client.save_state(1746 self.state_store_name,1747 stream_key,1748 events1749 )17501751 # Also save by event ID for direct lookup1752 event_key = f"event:{event.event_id}"1753 await self.dapr_client.save_state(1754 self.state_store_name,1755 event_key,1756 event.to_dict()1757 )17581759 # Update type index1760 type_index_key = f"type:{event.event_type}"1761 type_events = await self.dapr_client.get_state(1762 self.state_store_name,1763 type_index_key1764 ) or []1765 type_events.append(event.event_id)17661767 await self.dapr_client.save_state(1768 self.state_store_name,1769 type_index_key,1770 type_events1771 )17721773 async def get_events(1774 self,1775 stream_id: str,1776 from_version: Optional[int] = None,1777 to_version: Optional[int] = None,1778 limit: Optional[int] = None1779 ) -> AsyncIterator[BaseEvent]:1780 """Get events from Dapr state store"""1781 stream_key = f"stream:{stream_id}"1782 events_data = await self.dapr_client.get_state(1783 self.state_store_name,1784 stream_key1785 ) or []17861787 # Apply filters1788 if from_version is not None:1789 events_data = events_data[from_version:]1790 if to_version is not None:1791 events_data = events_data[:to_version]1792 if limit is not None:1793 events_data = events_data[:limit]17941795 for event_data in events_data:1796 yield BaseEvent.from_dict(event_data)1797```17981799## 5. Deployment Patterns18001801### Kubernetes with Dapr18021803```yaml1804# deployments/kubernetes/dapr-config.yaml1805apiVersion: dapr.io/v1alpha11806kind: Configuration1807metadata:1808 name: app-config1809 namespace: production1810spec:1811 tracing:1812 samplingRate: "1"1813 zipkin:1814 endpointAddress: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans"1815 metrics:1816 enabled: true1817 rules: |1818 - name: dapr-system1819 description: "Dapr system metrics"1820 default: true1821 - name: app1822 description: "Application metrics"1823 default: true1824 features:1825 - name: AppHealthCheck1826 enabled: true1827 api:1828 allowed:1829 - name: state1830 - name: invoke1831 - name: publish1832 - name: bindings1833 - name: secrets1834---1835apiVersion: dapr.io/v1alpha11836kind: Component1837metadata:1838 name: pubsub1839 namespace: production1840spec:1841 type: pubsub.kafka1842 version: v11843 metadata:1844 - name: brokers1845 value: "kafka-broker:9092"1846 - name: authRequired1847 value: "false"1848 - name: consumerID1849 value: "app-pubsub"1850 - name: compressionType1851 value: "snappy"1852 - name: batch_size1853 value: "100"1854 - name: batch_timeout1855 value: "10ms"1856---1857apiVersion: dapr.io/v1alpha11858kind: Component1859metadata:1860 name: statestore1861 namespace: production1862spec:1863 type: state.postgresql1864 version: v11865 metadata:1866 - name: connectionString1867 secretKeyRef:1868 name: postgres-secret1869 key: connection-string1870 - name: tableName1871 value: "app_state"1872 - name: metadataTableName1873 value: "app_state_metadata"1874 - name: keyPrefix1875 value: "none"1876---1877apiVersion: dapr.io/v1alpha11878kind: Component1879metadata:1880 name: secretstore1881 namespace: production1882spec:1883 type: secretstores.kubernetes1884 version: v11885 metadata:1886 - name: namespace1887 value: "production"1888```18891890```yaml1891# deployments/kubernetes/app-with-dapr.yaml1892apiVersion: apps/v11893kind: Deployment1894metadata:1895 name: event-service1896 namespace: production1897 labels:1898 app: event-service1899spec:1900 replicas: 31901 selector:1902 matchLabels:1903 app: event-service1904 template:1905 metadata:1906 labels:1907 app: event-service1908 annotations:1909 dapr.io/enabled: "true"1910 dapr.io/app-id: "event-service"1911 dapr.io/app-port: "8000"1912 dapr.io/enable-metrics: "true"1913 dapr.io/enable-profiling: "true"1914 dapr.io/config: "app-config"1915 dapr.io/log-as-json: "true"1916 dapr.io/log-level: "info"1917 dapr.io/app-protocol: "http"1918 spec:1919 containers:1920 - name: app1921 image: myregistry/event-service:latest1922 ports:1923 - containerPort: 80001924 env:1925 - name: DAPR_HTTP_PORT1926 value: "3500"1927 - name: DAPR_GRPC_PORT1928 value: "50001"1929 - name: APP_PORT1930 value: "8000"1931 - name: LOG_LEVEL1932 value: "info"1933 resources:1934 requests:1935 memory: "256Mi"1936 cpu: "100m"1937 limits:1938 memory: "512Mi"1939 cpu: "500m"1940 livenessProbe:1941 httpGet:1942 path: /healthz1943 port: 80001944 initialDelaySeconds: 301945 periodSeconds: 101946 readinessProbe:1947 httpGet:1948 path: /readyz1949 port: 80001950 initialDelaySeconds: 51951 periodSeconds: 51952 volumeMounts:1953 - name: config1954 mountPath: /app/config1955 readOnly: true1956 volumes:1957 - name: config1958 configMap:1959 name: app-config1960---1961apiVersion: v11962kind: Service1963metadata:1964 name: event-service1965 namespace: production1966spec:1967 selector:1968 app: event-service1969 ports:1970 - port: 801971 targetPort: 80001972 name: http1973 type: ClusterIP1974```19751976This event-driven architecture skill provides comprehensive patterns for building modern, distributed systems using event sourcing, CQRS, saga patterns, and Dapr integration. It's designed to be framework-agnostic and applicable to any domain requiring robust event-driven capabilities.19771978`★ Insight ─────────────────────────────────────`1979The event-driven architecture patterns shown here emphasize several key 2025 best practices:19801. **Schema Evolution**: Events include versioning and support for backward/forward compatibility19812. **Broker Abstraction**: Generic interfaces allow swapping Kafka, Redis, or cloud brokers without code changes19823. **Recovery Patterns**: Sagas with compensation and retry policies ensure system resilience19834. **Observability**: Built-in tracing, metrics, and structured logging for production monitoring19845. **Multi-tenancy**: Native support for tenant isolation at the event level19851986These patterns ensure your event-driven system can evolve, scale, and maintain reliability as requirements change.1987`─────────────────────────────────────────────────`