Design Patterns
Select and apply structural design patterns (Strategy, Factory, Adapter, Observer, etc.) to reduce complexity in class design. Use when evaluating whether a pattern solves a concrete problem, refactoring conditional logic, dealing with too many if/else statements or code smells, resolving inheritance problems, or deciding how to structure classes.
npx skills add github:pantheon-org/tekhne --skill software-engineering--design-principles--design-patternsStructural design patterns for solving recurring design problems with proven solutions.
When to Use
- Eliminating complex conditional logic (if/else chains, switch statements)
- Designing plugin/extension systems
- Integrating external APIs or legacy systems
- Separating concerns with clear interfaces
- Evaluating whether a pattern solves a concrete problem
When Not to Use
- Class-level SOLID violations (use solid-principles)
- Architectural boundary decisions (use clean-architecture)
- Testing strategy configuration (use testable-design)
- NEVER apply patterns preemptively for imagined future needs
Pattern Selection Workflow
Step 1: Identify the Design Problem
Output: Concrete problem statement without pattern jargon.
Ask:
- What conditional logic or coupling exists?
- What needs to vary independently?
- What external system needs integration?
Example:
Problem: Payment processing logic uses if/else chains to select processors.
Adding new processors requires editing existing code (OCP violation).
Step 2: Evaluate Pattern Fit
Output: Pattern choice with explicit win condition.
ALWAYS ask before choosing a pattern, and ensure the win condition is measurable, not aspirational:
- Does this pattern solve the concrete problem?
- What complexity does it remove vs. add?
- Can the team maintain it?
Example:
Pattern: Strategy (not Factory).
Win condition: Eliminates if/else chains for payment processor selection.
Cost: One interface + implementations. Benefit: Easy to add new processors.
Step 3: Document Pattern Application
Output: Pattern applied with before/after comparison.
Template:
Pattern: Strategy
Problem: if/else chain for payment processor selection
Solution: PaymentProcessor interface + implementations (Stripe, PayPal, etc.)
Validation: New processors added without editing existing code
Common Patterns
Strategy Pattern
Use when: Conditional logic selects between algorithms.
Example:
// Before: if/else chain
if (type === 'stripe') {
// Stripe logic
} else if (type === 'paypal') {
// PayPal logic
}
// After: Strategy pattern
interface PaymentProcessor {
process(amount: number): Promise<Receipt>
}
class StripeProcessor implements PaymentProcessor { /* ... */ }
class PayPalProcessor implements PaymentProcessor { /* ... */ }
// Select strategy
const processor = processors[type]
await processor.process(amount)
Factory Pattern
Use when: Object creation logic is complex or conditional.
Example:
// Before: constructor with complex logic
class Report {
constructor(type: string) {
if (type === 'pdf') {
// PDF setup
} else if (type === 'csv') {
// CSV setup
}
}
}
// After: Factory pattern
interface Report { generate(): string }
class ReportFactory {
create(type: string): Report {
switch (type) {
case 'pdf': return new PdfReport()
case 'csv': return new CsvReport()
default: throw new Error('Unknown type')
}
}
}
Adapter Pattern
Use when: Integrating external APIs or legacy systems with incompatible interfaces.
Example:
// External API (incompatible interface)
class LegacyEmailService {
sendMail(to: string, subject: string, body: string): void { /* ... */ }
}
// Domain interface
interface IEmailService {
send(email: Email): Promise<void>
}
// Adapter
class EmailServiceAdapter implements IEmailService {
constructor(private legacy: LegacyEmailService) {}
async send(email: Email): Promise<void> {
this.legacy.sendMail(email.to, email.subject, email.body)
}
}
Observer Pattern
Use when: Multiple components need to react to state changes.
Example:
interface Observer {
update(event: Event): void
}
class Subject {
private observers: Observer[] = []
attach(observer: Observer): void {
this.observers.push(observer)
}
notify(event: Event): void {
this.observers.forEach(o => o.update(event))
}
}
Anti-Corruption Layer
Use when: Integrating with external systems that should not pollute your domain.
Example:
// External API with poor design
interface ExternalUserAPI {
getUserData(id: number): { usr_nm: string, eml: string }
}
// Anti-Corruption Layer
class UserGateway {
constructor(private api: ExternalUserAPI) {}
async getUser(id: string): Promise<User> {
const data = this.api.getUserData(Number(id))
return new User({ name: data.usr_nm, email: data.eml })
}
}
Humble Object Pattern
Use when: Separating testable logic from hard-to-test infrastructure.
Example:
// Humble object (hard to test, minimal logic)
class HttpController {
constructor(private useCase: CreateOrderUseCase) {}
async handle(req: Request, res: Response): Promise<void> {
const input = this.mapRequest(req)
const output = await this.useCase.execute(input)
res.json(output)
}
}
// Testable logic (easy to test, no infrastructure)
class CreateOrderUseCase {
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
// Business logic here
}
}
Anti-Patterns
NEVER apply patterns preemptively
BAD: Add Factory pattern “in case” we need multiple implementations.
GOOD: Wait for concrete need (second implementation) before extracting pattern.
WHY: an interface designed for an imagined second implementation almost never matches the shape the real one turns out to need — you pay the abstraction cost twice, once for the guess and once to fix it.
NEVER use patterns to hide complexity
BAD: Wrap simple logic in Strategy/Factory/Builder for “future flexibility.”
GOOD: Start simple; refactor to pattern when complexity justifies it.
WHY: an unnecessary layer of indirection makes a reader trace through an interface and a registry to find one branch of logic that a plain if would have shown in one place.
NEVER cargo-cult patterns from other codebases
BAD: Copy design patterns because they “look professional.”
GOOD: Apply patterns to solve concrete problems in your codebase.
WHY: a pattern copied without its originating constraint (the reason Gang-of-Four Visitor exists, for instance) adds ceremony with none of the payoff, and the next maintainer inherits the ceremony without the context.
NEVER optimize before measurement
BAD: Add cache or pool because function “might be slow.”
GOOD: Measure baseline, optimize when threshold is exceeded.
WHY: an unmeasured cache adds invalidation bugs and memory overhead for a bottleneck that may not exist; the fix is frequently slower than the code it “optimizes.”
NEVER use Singleton for state shared across tests
BAD:
class ConfigStore {
private static instance: ConfigStore
static get(): ConfigStore { return this.instance ??= new ConfigStore() }
}
GOOD: Inject the config as a constructor dependency; construct a fresh instance per test.
WHY: a module-level singleton persists mutated state between test cases unless every test remembers to reset it — the resulting flaky failures depend on run order and are effectively required to reproduce.
NEVER reach for Observer/pub-sub for a direct two-party call
BAD: Emit a named event and have exactly one listener react to it, when a direct method call would do.
GOOD: Call the method directly; introduce Observer only when multiple, decoupled listeners genuinely need to react independently.
WHY: a single-listener event turns a traceable call stack into a stringly-typed contract — the reader has to grep for the event name across the codebase to find the one place that handles it, instead of following a function call.
Quick Commands
# Find Strategy/Factory candidates (conditional logic)
rg -n "if.*type.*==|switch.*\(type\)|instanceof" src
# Find Adapter candidates (external API usage)
rg -n "import.*from.*external|import.*sdk" src
# Find Observer candidates (event handling)
rg -n "addEventListener|on\(|emit\(|dispatch\(" src
References
Pattern Categories
Creational: Control object creation (Factory, Builder, Singleton)
Structural: Compose objects and classes (Adapter, Decorator, Facade)
Behavioral: Manage algorithms and responsibilities (Strategy, Observer, Command)
Pattern Reference Files
- Anti-corruption layer: references/adapt-anti-corruption-layer.md
- Anti-patterns: references/anti-patterns-and-frameworks.md
- Humble object: references/bound-humble-object.md
- Partial boundaries: references/bound-partial-boundaries.md
- Interface ownership: references/dep-interface-ownership.md
- Stable abstractions: references/dep-stable-abstractions.md
- Detailed examples: references/detailed-examples.md
- Logging abstraction: references/frame-logging-abstraction.md
- Main component: references/bound-main-component.md
- Common closure: references/comp-common-closure.md
- Reuse-release equivalence: references/comp-reuse-release-equivalence.md
- Design Patterns (Gang of Four)
- Refactoring Guru Design Patterns
- Martin Fowler Catalog
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
No eval scenarios available.
Build Anti-Corruption Layers for External Systems
When integrating with external systems that have different models, build an anti-corruption layer (ACL) that translates between their model and yours. Never let external concepts pollute your domain.
Incorrect (external model leaks into domain):
// Domain polluted with Stripe's model
public class Order {
private String id;
private List<OrderItem> items;
// Stripe-specific fields in domain
private String stripeCustomerId;
private String stripePaymentIntentId;
private String stripePaymentMethodId;
public void processPayment(Stripe stripe) {
// Domain directly calls Stripe
PaymentIntent intent = PaymentIntent.create(
PaymentIntentCreateParams.builder()
.setCustomer(this.stripeCustomerId)
.setPaymentMethod(this.stripePaymentMethodId)
.setAmount(this.calculateTotal().cents())
.setCurrency("usd")
.build()
);
this.stripePaymentIntentId = intent.getId();
}
}
// If we switch to PayPal, Order must change
Correct (anti-corruption layer isolates external model):
// domain/Order.java - Pure domain, no external concepts
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderItem> items;
private PaymentStatus paymentStatus;
public Money calculateTotal() {
return items.stream()
.map(OrderItem::lineTotal)
.reduce(Money.zero(), Money::add);
}
public void markPaid(PaymentReference reference) {
this.paymentStatus = PaymentStatus.paid(reference);
}
}
// domain/ports/PaymentProcessor.java - Domain-defined port
public interface PaymentProcessor {
PaymentResult charge(CustomerId customer, Money amount);
}
// Domain-defined value objects
public record PaymentResult(PaymentReference reference, PaymentStatus status) {}
public record PaymentReference(String value) {}
// infrastructure/stripe/StripePaymentProcessor.java - ACL
public class StripePaymentProcessor implements PaymentProcessor {
private final CustomerMappingRepository customerMapping;
public PaymentResult charge(CustomerId customerId, Money amount) {
// Translate domain CustomerId to Stripe customer
String stripeCustomerId = customerMapping.getStripeId(customerId);
// Call Stripe with their model
PaymentIntent intent = PaymentIntent.create(
PaymentIntentCreateParams.builder()
.setCustomer(stripeCustomerId)
.setAmount(amount.cents())
.setCurrency("usd")
.setConfirm(true)
.build()
);
// Translate Stripe response back to domain model
return new PaymentResult(
new PaymentReference(intent.getId()),
translateStatus(intent.getStatus())
);
}
private PaymentStatus translateStatus(String stripeStatus) {
return switch (stripeStatus) {
case "succeeded" -> PaymentStatus.COMPLETED;
case "processing" -> PaymentStatus.PENDING;
case "requires_action" -> PaymentStatus.REQUIRES_ACTION;
default -> PaymentStatus.FAILED;
};
}
}
Benefits:
- Domain model remains clean and business-focused
- Switch payment providers by implementing new ACL
- External API changes isolated to ACL
Reference: Anti-Corruption Layer Pattern
Anti-Patterns and Self-Assessment Frameworks
This file contains systematic NEVER lists and self-assessment frameworks for software design principles.
Table of Contents
- Dependency Direction Anti-Patterns
- Entity Design Anti-Patterns
- SOLID Principles Anti-Patterns
- Self-Assessment Frameworks
Dependency Direction Anti-Patterns
NEVER DO - Dependency Direction
- NEVER import framework code in domain entities because it couples business rules to volatile implementation details and makes testing impossible
- NEVER import infrastructure in application layer because it violates the dependency inversion principle and makes the app untestable
- NEVER create circular dependencies between modules because it creates tight coupling and deployment issues
- NEVER depend on concrete implementations from higher layers because it violates dependency inversion and reduces flexibility
- NEVER import UI components in business logic because it couples domain rules to presentation concerns
- NEVER import database schemas in domain entities because it couples business logic to data storage details
- NEVER import HTTP clients in domain services because it couples business logic to communication protocols
- NEVER import configuration in domain entities because it makes business rules dependent on deployment details
Entity Design Anti-Patterns
NEVER DO - Entity Design
- NEVER create anemic entities with only getters/setters because it scatters business logic across services and violates encapsulation
- NEVER expose entity internals through public fields because it breaks encapsulation and allows invalid state changes
- NEVER put infrastructure concerns in entities because it violates single responsibility and makes testing hard
- NEVER create entities without invariant validation because it allows invalid business states to persist
- NEVER use primitive types for domain concepts because it allows type mismatches and loses business meaning
- NEVER create entities that know about persistence because it couples business logic to data storage mechanisms
- NEVER mix entity validation with UI validation because business rules become dependent on presentation layer
- NEVER create god entities with multiple responsibilities because it violates SRP and becomes unmaintainable
SOLID Principles Anti-Patterns
Single Responsibility Principle (SRP)
NEVER DO - Single Responsibility
- NEVER create classes that have multiple reasons to change because changes become risky and affect unrelated functionality
- NEVER mix business logic with data access because it couples domain rules to storage mechanisms
- NEVER combine validation with persistence because rule changes affect database operations unnecessarily
- NEVER put UI logic in business classes because presentation changes break business functionality
- NEVER mix error handling with business logic because error formatting becomes coupled to domain rules
- NEVER combine configuration with business operations because deployment changes affect business behavior
- NEVER put logging in domain entities because infrastructure concerns pollute business logic
- NEVER create service classes that do everything because they become unmaintainable god objects
Open/Closed Principle (OCP)
NEVER DO - Open/Closed Principle
- NEVER use switch statements for extensible behavior because adding new cases requires modifying existing code
- NEVER modify existing classes to add new features because it risks breaking existing functionality
- NEVER hard-code type checks for polymorphic behavior because new types require code changes everywhere
- NEVER create if-else chains for strategy selection because new strategies require modifying selection logic
- NEVER put feature flags directly in business logic because new features modify core business code
- NEVER create monolithic classes that handle multiple scenarios because new scenarios break existing code
Interface Segregation Principle (ISP)
NEVER DO - Interface Segregation
- NEVER create fat interfaces with many methods because clients depend on methods they don’t use
- NEVER force clients to implement unused methods because it violates interface segregation
- NEVER create one-size-fits-all interfaces because different clients have different needs
- NEVER put optional methods in required interfaces because not all clients need all functionality
Dependency Inversion Principle (DIP)
NEVER DO - Dependency Inversion
- NEVER depend on concrete classes in high-level modules because it creates tight coupling to implementation details
- NEVER instantiate dependencies directly because it makes testing and flexibility impossible
- NEVER import low-level modules in high-level policies because it violates the dependency rule
- NEVER use global variables for dependency access because it creates hidden dependencies and testing issues
Self-Assessment Frameworks
Dependency Direction Self-Assessment
Ask yourself these questions when reviewing dependency relationships:
- Does this import violate the dependency rule? (Are dependencies pointing inward toward the domain?)
- Would this code still work if I swapped the implementation? (Am I depending on abstractions?)
- Can I test this code without the infrastructure? (Are my dependencies invertible?)
- Does this create a circular dependency? (Can I build these modules independently?)
- Would changing the database/framework break this code? (Am I coupled to implementation details?)
- Can I deploy these modules separately? (Are my boundaries clean?)
- Does this make the build order fragile? (Are my compilation dependencies stable?)
Entity Design Self-Assessment
Ask yourself these questions when designing or reviewing entities:
- Does this entity contain the business logic that operates on its data? (Rich vs Anemic)
- Can this entity get into an invalid state? (Are invariants enforced?)
- Does this entity depend on infrastructure concerns? (Is it pure domain logic?)
- Would a business expert recognize these methods and properties? (Is it expressed in domain language?)
- Can I create this entity in different valid states? (Are there proper construction methods?)
- Does changing business rules require changing only this entity? (Is the logic encapsulated?)
Single Responsibility Self-Assessment
Ask yourself these questions when evaluating class responsibility:
- How many different reasons would cause me to change this class? (Should be only one)
- Can I describe what this class does in a single, simple sentence? (Without using “and” or “or”)
- Do the methods in this class all operate on related data? (Cohesion check)
- Would different stakeholders want to change different parts of this class? (Separation of concerns)
- Can I extract any behavior into a separate class without losing meaning? (Is it doing too much?)
- Do I need to understand multiple domains to understand this class? (Cross-cutting concerns check)
Open/Closed Self-Assessment
Ask yourself these questions about extensibility:
- If I need to add a new feature, do I have to modify existing code? (Open for extension, closed for modification)
- Can I add new behavior through composition or inheritance? (Extension mechanisms available?)
- Are there switch statements or if-else chains that grow with new requirements? (Strategy pattern needed?)
- Do I have abstractions that allow different implementations? (Plugin points exist?)
- Would adding new functionality risk breaking existing features? (Stability of existing code)
Interface Segregation Self-Assessment
Ask yourself these questions about interface design:
- Do all clients use all methods in this interface? (No forced dependencies on unused methods)
- Would different types of clients benefit from different interfaces? (Client-specific interfaces)
- Are there methods that only some implementations can provide? (Interface bloat check)
- Can I group related methods into smaller, focused interfaces? (Cohesive interfaces)
Dependency Inversion Self-Assessment
Ask yourself these questions about dependencies:
- Do my high-level modules depend on low-level modules? (Should depend on abstractions)
- Can I easily swap implementations for testing? (Dependency injection working?)
- Do I have circular dependencies between layers? (Clean architecture boundaries)
- Are my dependencies pointing in the same direction as my architecture? (Consistent dependency flow)
These frameworks help guide decision-making and catch violations before they become problematic.
Use Humble Objects at Architectural Boundaries
The Humble Object pattern separates hard-to-test behaviors from easy-to-test behaviors. The “humble” part contains minimal logic and is hard to test; the substantial logic goes in a testable component.
Incorrect (logic mixed with hard-to-test framework code):
// React component with business logic
function OrderSummary({ orderId }: Props) {
const [order, setOrder] = useState<Order | null>(null)
const [discount, setDiscount] = useState<number>(0)
useEffect(() => {
fetch(`/api/orders/${orderId}`)
.then(r => r.json())
.then(data => {
setOrder(data)
// Business logic in component - hard to test
if (data.items.length > 5) {
setDiscount(data.total * 0.1)
} else if (data.customer.tier === 'gold') {
setDiscount(data.total * 0.05)
}
})
}, [orderId])
const finalTotal = order ? order.total - discount : 0
return (
<div>
<span>Subtotal: ${order?.total}</span>
<span>Discount: ${discount}</span>
<span>Total: ${finalTotal}</span>
</div>
)
}
// Testing requires React Testing Library + mocked fetch
Correct (humble view + testable presenter):
// Presenter - pure function, easy to test
interface OrderViewModel {
subtotal: string
discount: string
total: string
hasDiscount: boolean
}
function presentOrder(order: Order): OrderViewModel {
const discount = calculateDiscount(order)
const finalTotal = order.total - discount
return {
subtotal: formatCurrency(order.total),
discount: formatCurrency(discount),
total: formatCurrency(finalTotal),
hasDiscount: discount > 0
}
}
function calculateDiscount(order: Order): number {
if (order.items.length > 5) return order.total * 0.1
if (order.customer.tier === 'gold') return order.total * 0.05
return 0
}
// Humble view - no logic, just renders data
function OrderSummary({ viewModel }: { viewModel: OrderViewModel }) {
return (
<div>
<span>Subtotal: {viewModel.subtotal}</span>
{viewModel.hasDiscount && <span>Discount: {viewModel.discount}</span>}
<span>Total: {viewModel.total}</span>
</div>
)
}
// Container handles data fetching - also humble
function OrderSummaryContainer({ orderId }: Props) {
const { data: order } = useQuery(['order', orderId], fetchOrder)
if (!order) return <Loading />
return <OrderSummary viewModel={presentOrder(order)} />
}
// Test presenter without React
test('applies bulk discount for 6+ items', () => {
const order = { items: [1,2,3,4,5,6], total: 100, customer: { tier: 'standard' } }
const vm = presentOrder(order)
expect(vm.discount).toBe('$10.00')
})
Benefits:
- Business logic tested with simple unit tests
- UI tests only verify rendering, not logic
- 90%+ of logic covered by fast tests
Reference: Clean Architecture - Humble Object Pattern
Treat Main as a Plugin to the Application
The Main component is the lowest-level, dirtiest component. It creates all factories, strategies, and global facilities, then hands control to high-level abstractions. Treat it as a plugin that can be swapped.
Incorrect (Main mixed with application logic):
// main.go
func main() {
db := connectDatabase()
// Business logic in main
http.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
var order Order
json.NewDecoder(r.Body).Decode(&order)
// Validation logic
if order.Total < 0 {
http.Error(w, "Invalid total", 400)
return
}
// Direct database calls
db.Exec("INSERT INTO orders ...")
// Response formatting
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
http.ListenAndServe(":8080", nil)
}
// Cannot test without starting HTTP server
// Cannot run with different database
Correct (Main as composition root):
// main.go - Production entry point
func main() {
app := bootstrap(ProductionConfig{
DatabaseURL: os.Getenv("DATABASE_URL"),
Port: os.Getenv("PORT"),
})
app.Run()
}
// bootstrap.go - Composition root
func bootstrap(config Config) *Application {
// Create infrastructure
db := postgres.NewConnection(config.DatabaseURL)
// Create repositories (implement domain interfaces)
orderRepo := postgres.NewOrderRepository(db)
userRepo := postgres.NewUserRepository(db)
// Create use cases (depend on interfaces)
placeOrder := usecases.NewPlaceOrderUseCase(orderRepo)
getOrders := usecases.NewGetOrdersUseCase(orderRepo)
// Create controllers (call use cases)
orderController := controllers.NewOrderController(placeOrder, getOrders)
// Wire up HTTP routes
router := http.NewRouter()
router.POST("/orders", orderController.Create)
router.GET("/orders", orderController.List)
return &Application{
Router: router,
Port: config.Port,
}
}
// main_test.go - Test entry point
func TestMain(t *testing.T) {
app := bootstrap(TestConfig{
DatabaseURL: "memory://", // In-memory for tests
Port: "0", // Random port
})
// Test against app
}
// main_dev.go - Development entry point
func main() {
app := bootstrap(DevConfig{
DatabaseURL: "localhost:5432",
Port: "3000",
})
app.RunWithHotReload()
}
Benefits:
- Swap database, logging, configuration per environment
- Integration tests use real wiring with test doubles
- Dev/staging/prod share same composition logic
Reference: Clean Architecture - The Main Component
Use Partial Boundaries When Full Separation is Premature
When you anticipate needing a boundary but the cost of full separation is too high, implement a partial boundary. This prepares for future separation without premature over-engineering.
Incorrect (no boundary when one might be needed):
# Tightly coupled - if we ever need to split, massive refactor
class ReportService:
def generate_sales_report(self, start_date, end_date):
# Direct database access
conn = psycopg2.connect(DATABASE_URL)
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM sales
WHERE date BETWEEN %s AND %s
""", (start_date, end_date))
# Direct PDF generation
pdf = FPDF()
for row in cursor.fetchall():
pdf.cell(200, 10, txt=str(row))
return pdf.output()
# Cannot split report generation from data access without rewriting
Correct (partial boundary - same component, prepared for split):
# Strategy 1: Facade pattern (simplest partial boundary)
class ReportingFacade:
"""Single entry point, internal implementation can be refactored later."""
def generate_sales_report(self, request: ReportRequest) -> Report:
data = self._fetch_data(request)
return self._format_report(data, request.format)
def _fetch_data(self, request):
# Could become a separate DataAccess component later
return self.repository.get_sales(request.start, request.end)
def _format_report(self, data, format):
# Could become a separate ReportRenderer component later
if format == 'pdf':
return PdfRenderer().render(data)
return CsvRenderer().render(data)
# Strategy 2: Interface with single implementation
class SalesDataProvider(Protocol):
def get_sales(self, start: date, end: date) -> list[Sale]: ...
class PostgresSalesProvider:
def get_sales(self, start: date, end: date) -> list[Sale]:
# Implementation here
# Both live in same component, but interface enables future split
# When split needed: move interface to one component, impl to another
# Strategy 3: One-dimensional boundary
# Skip reciprocal interface - simpler but weaker protection
class ReportGenerator:
def __init__(self, data_provider: SalesDataProvider):
self.data_provider = data_provider # Depends on abstraction
# SalesDataProvider implementation knows about ReportGenerator
# Not fully decoupled, but good enough for now
When to use partial boundaries:
- You suspect a boundary will be needed, but not yet
- Team is small and deployment is unified
- Cost of full boundary outweighs current benefits
Reference: Clean Architecture - Partial Boundaries
Group Classes That Change Together
Classes that change for the same reasons and at the same times should be in the same component. This is the Common Closure Principle (CCP) - the Single Responsibility Principle for components.
Incorrect (classes that change together are separated):
src/
├── entities/
│ └── Invoice.ts # Changes when billing rules change
├── repositories/
│ └── InvoiceRepository.ts # Changes when billing rules change
├── services/
│ └── InvoiceService.ts # Changes when billing rules change
├── validators/
│ └── InvoiceValidator.ts # Changes when billing rules change
└── mappers/
└── InvoiceMapper.ts # Changes when billing rules change
# A billing rule change touches 5 directories
# Risk of forgetting one, inconsistent changes
Correct (classes that change together are grouped):
src/
├── billing/
│ ├── domain/
│ │ ├── Invoice.ts
│ │ ├── InvoiceLine.ts
│ │ ├── InvoiceRules.ts
│ │ └── InvoiceValidator.ts
│ ├── application/
│ │ ├── CreateInvoiceUseCase.ts
│ │ └── ports/
│ │ └── InvoiceRepository.ts
│ └── infrastructure/
│ ├── PostgresInvoiceRepository.ts
│ └── InvoiceMapper.ts
├── payments/
│ └── ... # Changes for different reasons
└── shipping/
└── ... # Changes for different reasons
# Billing rule change isolated to billing/
# Single PR, single review, single deployment
How to identify change reasons:
- “When X business rule changes, which classes change?”
- “When the Y team requests a feature, which code changes?”
- Group by actor/stakeholder, not by technical layer
Release Components as Cohesive Units
Classes grouped into a component should be releasable together. A component should have a version number, release notes, and clear documentation. This is the Reuse/Release Equivalence Principle (REP).
Incorrect (arbitrary grouping):
shared-lib/
├── src/
│ ├── auth/
│ │ ├── JwtValidator.ts
│ │ └── PermissionChecker.ts
│ ├── logging/
│ │ └── Logger.ts
│ ├── email/
│ │ └── EmailSender.ts
│ └── payment/
│ └── StripeClient.ts
└── package.json // version 1.2.3
# Version 1.2.4 fixes JWT bug
# But users of EmailSender must also upgrade
# Changelog unclear which features affect which users
Correct (cohesive releasable components):
packages/
├── auth/
│ ├── src/
│ │ ├── JwtValidator.ts
│ │ ├── PermissionChecker.ts
│ │ └── index.ts
│ ├── CHANGELOG.md # Auth-specific changes
│ └── package.json # @company/auth v2.1.0
├── logging/
│ ├── src/
│ │ └── Logger.ts
│ ├── CHANGELOG.md
│ └── package.json # @company/logging v1.0.3
├── email/
│ ├── src/
│ │ └── EmailSender.ts
│ ├── CHANGELOG.md
│ └── package.json # @company/email v1.5.0
└── payments/
├── src/
│ └── StripeClient.ts
├── CHANGELOG.md
└── package.json # @company/payments v3.0.0
# Clear ownership: auth team owns @company/auth
# Independent versioning: JWT fix only bumps auth
# Semantic versioning: breaking change in payments doesn't affect others
Benefits:
- Teams release independently on their own schedule
- Version numbers communicate compatibility
- Changelogs specific to what users of each component care about
Reference: Clean Architecture - Reuse/Release Equivalence Principle
Interfaces Belong to Clients Not Implementers
Interfaces should be defined in the layer that uses them, not the layer that implements them. The client owns the abstraction; the implementation adapts to it.
Incorrect (interface defined next to implementation):
// infrastructure/persistence/UserRepository.java
public interface UserRepository {
User findById(String id);
void save(User user);
}
// infrastructure/persistence/PostgresUserRepository.java
public class PostgresUserRepository implements UserRepository {
// Implementation
}
// application/usecases/CreateUserUseCase.java
import infrastructure.persistence.UserRepository; // Use case imports from infrastructure!
public class CreateUserUseCase {
private final UserRepository repository;
}
Correct (interface defined where it’s used):
// application/ports/output/UserRepository.java
public interface UserRepository {
User findById(String id);
void save(User user);
}
// application/usecases/CreateUserUseCase.java
import application.ports.output.UserRepository; // Same layer import
public class CreateUserUseCase {
private final UserRepository repository; // No infrastructure dependency
}
// infrastructure/persistence/PostgresUserRepository.java
import application.ports.output.UserRepository; // Infrastructure depends on application
public class PostgresUserRepository implements UserRepository {
// Implementation adapts to the port
}
Note: This is the essence of the Dependency Inversion Principle. The high-level module defines what it needs; low-level modules conform to that contract.
Reference: Clean Architecture - Chapter 11: DIP
Depend on Stable Abstractions Not Volatile Concretions
The most flexible systems depend on abstractions, not concretions. Volatile concrete classes are under active development and change frequently; depending on them propagates instability.
Incorrect (depending on volatile concrete class):
// services/notification.go
package services
import (
"myapp/infrastructure/email/sendgrid"
"myapp/infrastructure/sms/twilio"
)
type NotificationService struct {
email *sendgrid.Client // Concrete SendGrid client
sms *twilio.Client // Concrete Twilio client
}
func (n *NotificationService) NotifyUser(userID string, message string) {
// When SendGrid API changes, this service must change
// When migrating to AWS SES, this service must change
n.email.SendWithTemplate("notify", message)
}
Correct (depending on stable interface):
// domain/ports/notification.go
package ports
type EmailSender interface {
Send(to string, subject string, body string) error
}
type SMSSender interface {
Send(to string, message string) error
}
// services/notification.go
package services
import "myapp/domain/ports"
type NotificationService struct {
email ports.EmailSender // Stable abstraction
sms ports.SMSSender // Stable abstraction
}
func (n *NotificationService) NotifyUser(userID string, message string) {
// Service is immune to email provider changes
n.email.Send(userID, "Notification", message)
}
// infrastructure/sendgrid/client.go
package sendgrid
type Client struct { /* ... */ }
func (c *Client) Send(to, subject, body string) error {
// Concrete implementation can change freely
}
Note: Depending on stable concretions (like standard library classes) is acceptable. Focus inversion on volatile, actively-developed modules.
Reference: Clean Architecture - Stable Abstractions Principle
Detailed Implementation Examples
This file contains comprehensive code examples for applying software design principles across different programming paradigms.
Table of Contents
- Dependency Direction Examples
- Entity Design Examples
- SOLID Principles Examples
- Structural Design Examples
- Real-World Violation Examples
Dependency Direction Examples
TypeScript Clean Architecture
// GOOD: Domain entity with no external dependencies
export interface User {
id: UserId;
email: Email;
validateEmailChange(newEmail: Email): ValidationResult;
}
// GOOD: Application service depends on domain
export class UserService {
constructor(
private userRepository: UserRepository, // Interface from domain
private emailService: EmailService // Interface from domain
) {}
}
// BAD: Domain entity importing infrastructure
import { Database } from '@/infrastructure/database'; // ❌ Wrong direction
export class User {
save() {
return Database.insert(this); // ❌ Domain knows about infrastructure
}
}
Elixir Boundary Management
# GOOD: Domain context with pure business logic
defmodule Billing.Invoice do
@type t :: %__MODULE__{
id: String.t(),
amount: Money.t(),
status: :draft | :sent | :paid
}
def calculate_total(%__MODULE__{} = invoice) do
# Pure business calculation
invoice.line_items
|> Enum.reduce(Money.new(0), &Money.add(&2, &1.amount))
end
end
# GOOD: Infrastructure adapter
defmodule Billing.Adapters.InvoiceRepository do
def save(%Invoice{} = invoice) do
# Database operations here
end
end
# BAD: Domain importing infrastructure
defmodule Billing.Invoice do
alias Billing.Database.Repo # ❌ Wrong direction
def save(%__MODULE__{} = invoice) do
Repo.insert(invoice) # ❌ Domain knows about persistence
end
end
Entity Design Examples
Rich Domain Entities
// GOOD: Rich entity with business logic
export class Order {
private constructor(
private readonly id: OrderId,
private readonly customerId: CustomerId,
private items: OrderItem[],
private status: OrderStatus
) {}
static create(customerId: CustomerId, items: OrderItem[]): Result<Order, DomainError> {
if (items.length === 0) {
return Err(new DomainError("Order must have at least one item"));
}
return Ok(new Order(
OrderId.generate(),
customerId,
items,
OrderStatus.PENDING
));
}
addItem(item: OrderItem): Result<void, DomainError> {
if (this.status !== OrderStatus.PENDING) {
return Err(new DomainError("Cannot modify confirmed order"));
}
this.items.push(item);
return Ok(void 0);
}
calculateTotal(): Money {
return this.items.reduce(
(total, item) => total.add(item.getPrice()),
Money.zero()
);
}
}
// BAD: Anemic entity
export class Order {
public id: string;
public customerId: string;
public items: any[];
public status: string;
public total: number; // ❌ No business logic, just data
}
Elixir Rich Entities with Changesets
# GOOD: Rich entity with validation
defmodule Accounts.User do
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{
email: String.t(),
encrypted_password: String.t(),
status: :active | :suspended | :deleted
}
def registration_changeset(attrs \\ %{}) do
%__MODULE__{}
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
|> validate_email_format()
|> validate_password_strength()
|> encrypt_password()
end
def suspend(%__MODULE__{status: :active} = user) do
user
|> change(status: :suspended)
|> validate_suspension_allowed()
end
def suspend(%__MODULE__{status: status}) do
{:error, "Cannot suspend user with status: #{status}"}
end
# Business rule enforcement
defp validate_suspension_allowed(changeset) do
# Check business rules for suspension
case get_field(changeset, :subscription_status) do
:premium -> add_error(changeset, :status, "Premium users cannot be suspended")
_ -> changeset
end
end
end
SOLID Principles Examples
Single Responsibility Principle
// BAD: Violates SRP
export class UserManager {
validateUser(user: User): boolean { /* validation */ }
saveUser(user: User): void { /* database operation */ }
sendWelcomeEmail(user: User): void { /* email sending */ }
hashPassword(password: string): string { /* cryptography */ }
logUserActivity(user: User, action: string): void { /* logging */ }
}
// GOOD: Separated responsibilities
export class UserValidator {
validate(user: User): ValidationResult {
// Only validation logic
}
}
export class UserRepository {
save(user: User): Promise<void> {
// Only persistence logic
}
}
export class UserNotificationService {
sendWelcomeEmail(user: User): Promise<void> {
// Only notification logic
}
}
Open/Closed Principle
// BAD: Must modify for new discount types
class DiscountCalculator {
calculate(type: string, amount: number): number {
switch (type) {
case 'percentage':
return amount * 0.1;
case 'fixed':
return Math.min(amount, 50);
// ❌ Adding new types requires modification
default:
return 0;
}
}
}
// GOOD: Open for extension, closed for modification
interface DiscountStrategy {
calculate(amount: number): number;
}
class PercentageDiscount implements DiscountStrategy {
constructor(private rate: number) {}
calculate(amount: number): number {
return amount * this.rate;
}
}
class FixedAmountDiscount implements DiscountStrategy {
constructor(private discountAmount: number) {}
calculate(amount: number): number {
return Math.min(amount, this.discountAmount);
}
}
class DiscountCalculator {
calculate(strategy: DiscountStrategy, amount: number): number {
return strategy.calculate(amount);
}
}
Real-World Violation Examples
GraphQL Resolver Violations
// BAD: Violates multiple principles
export class UserResolver {
async getUser(id: string) {
// ❌ SRP violation: validation, business logic, data access all mixed
// Manual validation (should be in validator)
if (!id || id.length < 1) {
throw new Error("Invalid ID");
}
// Database access (should be in repository)
const user = await database.users.findOne({ id });
if (!user) {
throw new Error("User not found");
}
// Business logic (should be in entity/service)
if (user.subscription_expires < new Date()) {
user.status = 'expired';
await database.users.update({ id }, { status: 'expired' });
}
// Email sending (should be in notification service)
if (user.last_login < thirtyDaysAgo()) {
await emailService.send(user.email, 'comeback-email');
}
// Logging (should be in logging service)
await database.audit_log.insert({
user_id: id,
action: 'user_accessed',
timestamp: new Date()
});
// Data transformation (should be in presenter/mapper)
return {
id: user.id,
name: user.first_name + ' ' + user.last_name,
email: user.email.toLowerCase(),
avatar: `https://cdn.example.com/avatars/${user.id}.jpg`
};
}
}
// GOOD: Properly separated
export class UserResolver {
constructor(
private userService: UserService,
private userPresenter: UserPresenter
) {}
async getUser(id: string): Promise<UserDto> {
const user = await this.userService.getById(new UserId(id));
return this.userPresenter.toDto(user);
}
}
React Component Violations
// BAD: Component doing too much
const UserDashboard: React.FC<{ userId: string }> = ({ userId }) => {
const [user, setUser] = useState<User>();
const [subscription, setSubscription] = useState<Subscription>();
const [notifications, setNotifications] = useState<Notification[]>([]);
useEffect(() => {
// ❌ Data fetching logic in component
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(userData => {
setUser(userData);
// ❌ Business logic in component
if (userData.subscriptionId) {
fetch(`/api/subscriptions/${userData.subscriptionId}`)
.then(subResponse => subResponse.json())
.then(subData => {
setSubscription(subData);
// ❌ More business logic
if (subData.expires < Date.now()) {
// Send expiry email
fetch('/api/notifications/expiry', {
method: 'POST',
body: JSON.stringify({ userId, email: userData.email })
});
}
});
}
// ❌ Analytics tracking in component
analytics.track('user_dashboard_viewed', {
userId,
timestamp: Date.now()
});
});
}, [userId]);
// ❌ Complex rendering logic mixed with business logic
return (
<div>
{user && (
<>
<h1>Welcome {user.firstName} {user.lastName}</h1>
{subscription?.expires < Date.now() ? (
<div className="alert-danger">
Your subscription expired!
<button onClick={() => {
// ❌ Payment logic in component
stripe.redirectToCheckout({
sessionId: subscription.renewalSessionId
});
}}>
Renew Now
</button>
</div>
) : (
<div className="subscription-active">
Active until {new Date(subscription.expires).toLocaleDateString()}
</div>
)}
</>
)}
</div>
);
};
// GOOD: Separated concerns
const UserDashboard: React.FC<{ userId: string }> = ({ userId }) => {
const { user, subscription, loading } = useUserDashboardData(userId);
if (loading) return <LoadingSpinner />;
return (
<div>
<UserHeader user={user} />
<SubscriptionStatus subscription={subscription} />
</div>
);
};
Structural Design Examples
Composition Over Inheritance
// BAD: Inheritance hierarchy
class Animal {
move() { /* basic movement */ }
eat() { /* basic eating */ }
}
class Bird extends Animal {
fly() { /* flying logic */ }
move() { /* override with flying */ }
}
class Penguin extends Bird {
fly() { throw new Error("Penguins can't fly!"); } // ❌ LSP violation
move() { /* swimming logic */ }
}
// GOOD: Composition
interface MovementBehavior {
move(): void;
}
class FlyingMovement implements MovementBehavior {
move(): void { /* flying logic */ }
}
class SwimmingMovement implements MovementBehavior {
move(): void { /* swimming logic */ }
}
class Animal {
constructor(private movementBehavior: MovementBehavior) {}
move(): void {
this.movementBehavior.move();
}
}
// Usage
const eagle = new Animal(new FlyingMovement());
const penguin = new Animal(new SwimmingMovement());
Law of Demeter Examples
// BAD: Violates Law of Demeter
class Customer {
getOrder() {
return this.order;
}
}
class Order {
getDelivery() {
return this.delivery;
}
}
class Delivery {
getAddress() {
return this.address;
}
}
// ❌ Client reaches through multiple objects
const customerCity = customer.getOrder().getDelivery().getAddress().city;
// GOOD: Delegate to owning objects
class Customer {
getDeliveryCity(): string {
return this.order.getDeliveryCity();
}
}
class Order {
getDeliveryCity(): string {
return this.delivery.getCity();
}
}
class Delivery {
getCity(): string {
return this.address.city;
}
}
// ✅ Clean interface
const customerCity = customer.getDeliveryCity();
This reference file provides comprehensive examples that can be referenced from the main SKILL.md file without cluttering it with detailed code samples.
Abstract Logging Behind Domain Interfaces
Logging frameworks are infrastructure details. Define logging interfaces in the application layer; implement them in infrastructure. This enables switching loggers and keeps domain pure.
Incorrect (logging framework in domain/application):
package usecases
import (
"github.com/sirupsen/logrus" // Framework dependency
)
type CreateOrderUseCase struct {
repo OrderRepository
logger *logrus.Logger // Concrete logger
}
func (uc *CreateOrderUseCase) Execute(cmd CreateOrderCommand) (*Order, error) {
uc.logger.WithFields(logrus.Fields{
"customer_id": cmd.CustomerID,
"item_count": len(cmd.Items),
}).Info("Creating order")
order, err := uc.repo.Create(cmd)
if err != nil {
uc.logger.WithError(err).Error("Failed to create order")
return nil, err
}
uc.logger.WithField("order_id", order.ID).Info("Order created")
return order, nil
}
// Switching from logrus to zap requires changing all use cases
Correct (logging behind interface):
// application/ports/logger.go
package ports
type Logger interface {
Info(msg string, fields ...Field)
Error(msg string, err error, fields ...Field)
Debug(msg string, fields ...Field)
}
type Field struct {
Key string
Value interface{}
}
func F(key string, value interface{}) Field {
return Field{Key: key, Value: value}
}
// application/usecases/create_order.go
package usecases
import "myapp/application/ports"
type CreateOrderUseCase struct {
repo OrderRepository
logger ports.Logger
}
func (uc *CreateOrderUseCase) Execute(cmd CreateOrderCommand) (*Order, error) {
uc.logger.Info("Creating order",
ports.F("customer_id", cmd.CustomerID),
ports.F("item_count", len(cmd.Items)),
)
order, err := uc.repo.Create(cmd)
if err != nil {
uc.logger.Error("Failed to create order", err,
ports.F("customer_id", cmd.CustomerID),
)
return nil, err
}
uc.logger.Info("Order created", ports.F("order_id", order.ID))
return order, nil
}
// infrastructure/logging/logrus_logger.go
package logging
import (
"github.com/sirupsen/logrus"
"myapp/application/ports"
)
type LogrusLogger struct {
logger *logrus.Logger
}
func (l *LogrusLogger) Info(msg string, fields ...ports.Field) {
l.logger.WithFields(toLogrusFields(fields)).Info(msg)
}
func toLogrusFields(fields []ports.Field) logrus.Fields {
result := logrus.Fields{}
for _, f := range fields {
result[f.Key] = f.Value
}
return result
}
// infrastructure/logging/zap_logger.go - Alternative implementation
// Switch without touching use cases
Benefits:
- Application code doesn’t import logging frameworks
- Easy to switch logging backends
- Test logging by asserting on mock logger calls
Reference: Clean Architecture - Frameworks are Details