| name | architecture-patterns |
| type | knowledge |
| description | This skill should be used when designing system architecture, making architectural decisions, or evaluating design patterns. It provides guidance on common patterns, ADR templates, design principles, and tradeoff analysis. |
| keywords | architecture, design, pattern, decision, tradeoffs, adr, system design, scalability, microservices, mvc, design patterns, solid |
| auto_activate | true |
Architecture Patterns Skill
Architectural design patterns, decision frameworks, and system design principles.
When This Skill Activates
- Designing system architecture
- Writing Architecture Decision Records (ADRs)
- Evaluating design patterns
- Making architectural tradeoffs
- System design questions
- Keywords: "architecture", "design", "pattern", "adr", "system design", "scalability"
Architecture Decision Records (ADRs)
What is an ADR?
An ADR documents an architectural decision - the context, the decision made, and the consequences.
When to Write an ADR
Write an ADR for decisions that:
- Are hard to reverse
- Impact multiple teams
- Involve significant tradeoffs
- Set precedents for future work
Examples:
- ✅ "We chose PostgreSQL over MongoDB"
- ✅ "We split the monolith into microservices"
- ✅ "We adopted event-driven architecture"
- ❌ "We renamed a function" (too trivial)
- ❌ "We fixed a bug" (not architectural)
ADR Template
# ADR-### [Short Title]
**Date**: YYYY-MM-DD
**Status**: [Proposed | Accepted | Deprecated | Superseded]
**Deciders**: [Names/Roles]
## Context
What is the issue we're trying to solve? What are the constraints?
Example:
> Our monolithic application has grown to 200K lines of code.
> Deploy times are 45+ minutes, and teams are blocked on each other.
> We need to improve deployment speed and team autonomy.
## Decision
What did we decide to do?
Example:
> We will split the monolith into domain-driven microservices,
> starting with the user service and order service.
## Alternatives Considered
What other options did we evaluate?
### Option 1: Keep the monolith
**Pros**: No migration cost, simpler deployment
**Cons**: Deploy times won't improve, team blocking continues
### Option 2: Modular monolith
**Pros**: Better than status quo, no network calls
**Cons**: Still single deployment unit, doesn't solve deploy time
### Option 3: Microservices (chosen)
**Pros**: Independent deploys, team autonomy, scalability
**Cons**: Complexity, network calls, distributed system challenges
## Consequences
### Positive
- Deploy times drop from 45min to 5min per service
- Teams can deploy independently
- Services can scale independently
### Negative
- Need service mesh (added complexity)
- Distributed tracing required
- Data consistency challenges
### Neutral
- Migration will take 6 months
- Need to train team on distributed systems
## Implementation Notes
- Phase 1: Extract user service (Month 1-2)
- Phase 2: Extract order service (Month 3-4)
- Phase 3: Extract payment service (Month 5-6)
- Use API gateway for routing
- Adopt Kubernetes for orchestration
## References
- [Martin Fowler - Microservices](https://martinfowler.com/articles/microservices.html)
- Internal: `docs/microservices-migration-plan.md`
---
**Supersedes**: [ADR-005] if this replaces an earlier decision
**Superseded by**: [ADR-015] if a later decision overrides this
ADR Lifecycle
- Proposed: Draft for review
- Accepted: Team approved, implementing
- Deprecated: No longer recommended, but not replaced
- Superseded: Replaced by a newer ADR
Common Architecture Patterns
1. Layered (N-Tier) Architecture
Structure:
┌─────────────────────────┐
│ Presentation Layer │ (UI, Controllers)
├─────────────────────────┤
│ Business Logic │ (Services, Domain)
├─────────────────────────┤
│ Data Access Layer │ (Repositories, ORM)
├─────────────────────────┤
│ Database │ (PostgreSQL, MySQL)
└─────────────────────────┘
When to use: Traditional web applications, CRUD-heavy systems
Pros:
- Simple to understand and implement
- Clear separation of concerns
- Easy to test each layer independently
Cons:
- Can become monolithic
- Changes ripple through layers
- Performance overhead from layer boundaries
Example use case: E-commerce website, internal business tools
2. Microservices Architecture
Structure:
┌────────────┐ ┌────────────┐ ┌────────────┐
│ User │ │ Order │ │ Payment │
│ Service │ │ Service │ │ Service │
└────────────┘ └────────────┘ └────────────┘
│ │ │
└───────────────┴───────────────┘
│
┌──────────────┐
│ API Gateway │
└──────────────┘
When to use: Large teams, independent deployment needs, high scalability requirements
Pros:
- Independent deployment and scaling
- Team autonomy
- Technology diversity possible
- Fault isolation
Cons:
- Distributed system complexity
- Network latency
- Data consistency challenges
- Higher operational overhead
Example use case: Netflix, Amazon, large-scale SaaS platforms
3. Event-Driven Architecture
Structure:
┌────────────┐ ┌────────────┐
│ Service A │───► Event Bus ───►│ Service B │
└────────────┘ (Kafka) └────────────┘
│
▼
┌────────────┐
│ Service C │
└────────────┘
When to use: Real-time systems, async workflows, event sourcing
Pros:
- Loose coupling between services
- Highly scalable
- Natural fit for real-time/streaming
- Easy to add new consumers
Cons:
- Debugging is harder (distributed traces)
- Event ordering challenges
- At-least-once/exactly-once semantics complexity
Example use case: Stock trading platforms, IoT systems, real-time analytics
4. Hexagonal Architecture (Ports & Adapters)
Structure:
┌──────────────────────┐
│ Domain Logic │
│ (Business Rules) │
└──────────────────────┘
▲ ▲
│ │
┌─────┘ └─────┐
│ │
┌────▼────┐ ┌────▼────┐
│ HTTP │ │Database │
│ Adapter │ │ Adapter │
└─────────┘ └─────────┘
When to use: Domain-driven design, testability is critical
Pros:
- Business logic isolated from infrastructure
- Easy to test (mock adapters)
- Easy to swap implementations (e.g., swap database)
Cons:
- More initial setup
- Can be over-engineering for simple CRUD
Example use case: Banking systems, healthcare applications (domain-heavy)
5. Serverless Architecture
Structure:
API Gateway → Lambda → DynamoDB
→ Lambda → S3
→ Lambda → SQS
When to use: Variable/unpredictable load, event-driven tasks
Pros:
- No server management
- Pay-per-use pricing
- Auto-scaling
- Fast to deploy
Cons:
- Cold start latency
- Vendor lock-in
- Debugging is harder
- Limited execution time
Example use case: Image processing, webhooks, scheduled jobs
Design Patterns (Gang of Four)
Creational Patterns
Singleton
Purpose: Ensure only one instance exists
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connection = create_connection()
return cls._instance
# Usage
db1 = DatabaseConnection() # Creates instance
db2 = DatabaseConnection() # Returns same instance
assert db1 is db2 # True
When to use: Shared resources (DB connection, config, cache)
Caution: Can make testing difficult (global state)
Factory Pattern
Purpose: Create objects without specifying exact class
class PaymentProcessorFactory:
@staticmethod
def create(payment_type: str):
if payment_type == "credit_card":
return CreditCardProcessor()
elif payment_type == "paypal":
return PayPalProcessor()
elif payment_type == "crypto":
return CryptoProcessor()
raise ValueError(f"Unknown payment type: {payment_type}")
# Usage
processor = PaymentProcessorFactory.create("credit_card")
processor.process(amount=100)
When to use: Object creation logic is complex or conditional
Structural Patterns
Adapter Pattern
Purpose: Make incompatible interfaces work together
# Legacy system
class OldLogger:
def log_message(self, msg):
print(f"[OLD] {msg}")
# New interface
class Logger:
def log(self, level, message):
pass
# Adapter
class OldLoggerAdapter(Logger):
def __init__(self, old_logger):
self.old_logger = old_logger
def log(self, level, message):
self.old_logger.log_message(f"{level}: {message}")
# Usage
old = OldLogger()
adapter = OldLoggerAdapter(old)
adapter.log("INFO", "System started") # Works with new interface!
When to use: Integrating legacy code, third-party libraries
Decorator Pattern
Purpose: Add behavior to objects dynamically
# Base
class Coffee:
def cost(self):
return 2.00
# Decorators
class MilkDecorator:
def __init__(self, coffee):
self.coffee = coffee
def cost(self):
return self.coffee.cost() + 0.50
class SugarDecorator:
def __init__(self, coffee):
self.coffee = coffee
def cost(self):
return self.coffee.cost() + 0.25
# Usage
coffee = Coffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
print(coffee.cost()) # 2.75
When to use: Add responsibilities without subclassing
Behavioral Patterns
Strategy Pattern
Purpose: Select algorithm at runtime
from abc import ABC, abstractmethod
class TrainingStrategy(ABC):
@abstractmethod
def train(self, model, data):
pass
class LoRAStrategy(TrainingStrategy):
def train(self, model, data):
# LoRA-specific training
pass
class DPOStrategy(TrainingStrategy):
def train(self, model, data):
# DPO-specific training
pass
class Trainer:
def __init__(self, strategy: TrainingStrategy):
self.strategy = strategy
def run(self, model, data):
self.strategy.train(model, data)
# Usage
trainer = Trainer(LoRAStrategy())
trainer.run(model, data)
# Switch strategy
trainer.strategy = DPOStrategy()
trainer.run(model, data)
When to use: Multiple algorithms, select at runtime
Observer Pattern
Purpose: Notify dependents when state changes
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def notify(self, event):
for observer in self._observers:
observer.update(event)
class Logger:
def update(self, event):
print(f"[LOG] {event}")
class EmailNotifier:
def update(self, event):
print(f"[EMAIL] Sending alert for: {event}")
# Usage
order_system = Subject()
order_system.attach(Logger())
order_system.attach(EmailNotifier())
order_system.notify("Order placed") # Both observers notified
When to use: Event systems, publish-subscribe patterns
System Design Principles
SOLID Principles
S - Single Responsibility
Rule: A class should have ONE reason to change
# ❌ BAD: Multiple responsibilities
class User:
def save_to_database(self): ...
def send_email(self): ...
def generate_report(self): ...
# ✅ GOOD: Single responsibility
class User:
pass
class UserRepository:
def save(self, user): ...
class EmailService:
def send_welcome_email(self, user): ...
class ReportGenerator:
def generate_user_report(self, user): ...
O - Open/Closed
Rule: Open for extension, closed for modification
# ❌ BAD: Must modify class to add new shapes
class AreaCalculator:
def calculate(self, shapes):
total = 0
for shape in shapes:
if shape.type == "circle":
total += 3.14 * shape.radius ** 2
elif shape.type == "square":
total += shape.side ** 2
return total
# ✅ GOOD: Extend via new classes
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def area(self):
return 3.14 * self.radius ** 2
class Square(Shape):
def area(self):
return self.side ** 2
class AreaCalculator:
def calculate(self, shapes):
return sum(shape.area() for shape in shapes)
L - Liskov Substitution
Rule: Subtypes must be substitutable for base types
# ❌ BAD: Violates LSP
class Bird:
def fly(self): pass
class Penguin(Bird): # Penguins can't fly!
def fly(self):
raise NotImplementedError("Penguins can't fly")
# ✅ GOOD: Proper hierarchy
class Bird:
pass
class FlyingBird(Bird):
def fly(self): pass
class Sparrow(FlyingBird):
def fly(self): ...
class Penguin(Bird):
def swim(self): ...
I - Interface Segregation
Rule: Many specific interfaces > one general interface
# ❌ BAD: Fat interface
class Worker:
def work(self): pass
def eat(self): pass
class Robot(Worker): # Robots don't eat!
def eat(self):
raise NotImplementedError()
# ✅ GOOD: Segregated interfaces
class Workable:
def work(self): pass
class Eatable:
def eat(self): pass
class Human(Workable, Eatable):
def work(self): ...
def eat(self): ...
class Robot(Workable):
def work(self): ...
D - Dependency Inversion
Rule: Depend on abstractions, not concretions
# ❌ BAD: Depends on concrete class
class EmailService:
pass
class NotificationManager:
def __init__(self):
self.email = EmailService() # Hard dependency!
# ✅ GOOD: Depends on abstraction
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, message): pass
class EmailNotifier(Notifier):
def send(self, message): ...
class SMSNotifier(Notifier):
def send(self, message): ...
class NotificationManager:
def __init__(self, notifier: Notifier):
self.notifier = notifier # Depends on abstraction
Other Key Principles
DRY (Don't Repeat Yourself)
Rule: Every piece of knowledge should have a single representation
# ❌ BAD: Duplicated validation
def create_user(email):
if "@" not in email:
raise ValueError("Invalid email")
...
def update_user(email):
if "@" not in email: # Duplicated!
raise ValueError("Invalid email")
...
# ✅ GOOD: Single source of truth
def validate_email(email):
if "@" not in email:
raise ValueError("Invalid email")
def create_user(email):
validate_email(email)
...
def update_user(email):
validate_email(email)
...
KISS (Keep It Simple, Stupid)
Rule: Simplest solution that works
# ❌ BAD: Over-engineered
class AbstractFactoryBuilderSingletonProxy:
def create_instance_with_dependency_injection():
...
# ✅ GOOD: Simple and clear
def create_user(name, email):
return User(name=name, email=email)
YAGNI (You Aren't Gonna Need It)
Rule: Don't add functionality until needed
# ❌ BAD: Adding features "just in case"
class User:
def export_to_json(self): ...
def export_to_xml(self): ... # Do we need XML?
def export_to_yaml(self): ... # Do we need YAML?
def export_to_csv(self): ... # Do we need CSV?
# ✅ GOOD: Only what's needed now
class User:
def export_to_json(self): # Only JSON needed right now
...
Tradeoff Analysis Framework
Performance vs. Simplicity
| Approach | Performance | Simplicity | When to Use |
|---|---|---|---|
| In-memory cache | ⭐⭐⭐ | ⭐⭐ | Hot data, read-heavy |
| Database query | ⭐ | ⭐⭐⭐ | Simple CRUD, occasional access |
| Redis cache | ⭐⭐ | ⭐ | Distributed caching needed |
Consistency vs. Availability (CAP Theorem)
Rule: In a distributed system, you can have at most 2 of 3:
- Consistency: All nodes see same data
- Availability: Every request gets a response
- Partition tolerance: System works despite network splits
Choices:
- CP: Consistency + Partition Tolerance (e.g., MongoDB, HBase)
- AP: Availability + Partition Tolerance (e.g., Cassandra, DynamoDB)
- CA: Not possible in distributed systems (network partitions happen!)
Coupling vs. Cohesion
Goal: Low coupling, high cohesion
# ❌ BAD: High coupling, low cohesion
class OrderProcessor:
def __init__(self, db, email, payment, inventory):
self.db = db
self.email = email
self.payment = payment
self.inventory = inventory # Coupled to 4 systems!
# ✅ GOOD: Low coupling, high cohesion
class OrderProcessor:
def __init__(self, order_repository):
self.repository = order_repository
class PaymentService:
def process(self, order): ...
class InventoryService:
def reserve(self, items): ...
Integration with [PROJECT_NAME]
[PROJECT_NAME] architectural principles:
- Patterns: Hexagonal architecture, event-driven for async tasks
- ADRs: Document all major decisions in
docs/adr/ - Principles: SOLID, DRY, KISS, YAGNI
- Design reviews: All major architecture changes reviewed before implementation
- Tradeoff documentation: Explain WHY we chose an approach
Additional Resources
Books:
- "Design Patterns" by Gang of Four
- "Clean Architecture" by Robert Martin
- "Software Architecture: The Hard Parts" by Neal Ford
- "Building Microservices" by Sam Newman
Websites:
Version: 1.0.0 Type: Knowledge skill (no scripts) See Also: documentation-guide (for ADR formatting), python-standards, code-review