architecture-design
Architecture & Design Principles Skill
Transform codebases into maintainable, testable, and scalable architectures using Clean Architecture principles, SOLID design patterns, and industry best practices from the Essential Developer methodology.
Overview
This skill guides you through a structured 5-step process to:
- Analyze Requirements → Identify components, responsibilities, and boundaries
- Apply SOLID Principles → Design with SRP, OCP, LSP, ISP, DIP
- Define Clean Architecture → Establish layers, boundaries, and dependency rules
- Design Testing Strategy → Plan unit tests, integration tests, and test boundaries
- Document Decisions → Create Architecture Decision Records (ADRs)
Core Philosophy
"Good architecture is a byproduct of good team processes and communication"
This skill follows these principles:
- Framework Independence - Business logic doesn't depend on frameworks
- Testability - Architecture enables easy testing
- UI Independence - UI can change without affecting business rules
- Database Independence - Business rules don't know about the database
- External Agency Independence - Business rules don't depend on external services
The 5-Step Process
Step 1: Analyze Requirements
Objective: Identify components, responsibilities, and architectural boundaries
Actions:
- Break down feature into distinct responsibilities
- Identify core business logic vs infrastructure concerns
- Recognize cross-cutting concerns (logging, analytics, caching)
- Map data flow through the system
- Identify potential architectural boundaries
Key Questions to Ask:
- What is the core business logic?
- What are the external dependencies (network, database, UI)?
- What needs to be testable in isolation?
- What components might change independently?
- What are the inputs and outputs of each component?
Output: Component diagram with clear responsibilities
Reference: See references/modular_design.md for component identification patterns
Step 2: Apply SOLID Principles
Objective: Design components following SOLID principles
S - Single Responsibility Principle (SRP)
Each class/module has one reason to change. Separate business logic from infrastructure.
- ❌ A class that loads data AND presents it
- ✅ Separate
UseCasefor business logic; separatePresenterfor view logic
O - Open/Closed Principle (OCP)
Open for extension via protocols/interfaces, closed for modification:
// Open for extension via protocol (Swift example — same idea in any language)
protocol ItemLoader {
func load() async throws -> [Item]
}
// Closed for modification — implementations extend behavior
final class RemoteItemLoader: ItemLoader { ... }
final class CachedItemLoader: ItemLoader { ... }
final class FallbackItemLoader: ItemLoader { ... }
L - Liskov Substitution Principle (LSP)
Subtypes must be substitutable for base types. Contracts must be honored by all implementations.
I - Interface Segregation Principle (ISP)
Clients shouldn't depend on interfaces they don't use. Create focused, specific protocols.
- ❌
protocol DataStore { func save(_:); func load(); func delete(); func migrate(); func backup() } - ✅
protocol ItemReader { func retrieve() throws -> [Item]? }+ separateItemCache
D - Dependency Inversion Principle (DIP)
High-level modules depend on abstractions, not concretions.
class ItemListViewController {
private let loader: ItemLoader // depends on abstraction, not concrete type
init(loader: ItemLoader) { self.loader = loader }
}
Output: SOLID-compliant component design
Reference: See references/solid_principles.md for detailed patterns and anti-patterns
Step 3: Define Clean Architecture
Objective: Establish clear architectural layers with proper dependency flow
The Clean Architecture Layers:
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ (UI, ViewModels, Presenters) │
└─────────────────┬───────────────────────┘
│ depends on
┌─────────────────▼───────────────────────┐
│ Domain/Business Layer │
│ (Use Cases, Entities, Rules) │
└─────────────────┬───────────────────────┘
│ depends on
┌─────────────────▼───────────────────────┐
│ Infrastructure Layer │
│ (Network, Database, Framework Code) │
└─────────────────────────────────────────┘
Dependency Rule: Source code dependencies point inward only
Key Patterns:
- Use Cases — contain business rules, orchestrate data flow, independent of UI and frameworks
- Boundaries (Protocols) — define contracts between layers, enable testability
- Adapters — convert data between layers, implement boundary protocols
- Composition Root — wire dependencies together, configure the object graph
Output: Layered architecture with clear boundaries
Reference: See references/clean_architecture.md for detailed layer definitions; examples/generic/layered_architecture.md for a language-agnostic example; examples/swift/ for Swift/iOS real implementations
Step 4: Design Testing Strategy
Objective: Plan comprehensive testing at all architectural layers
Testing Pyramid:
┌──────────┐
│ UI │ Few - End to End
├──────────┤
│Integration│ Some - Integration
├──────────┤
│ Unit │ Many - Fast & Isolated
└──────────┘
Testing Boundaries:
- Domain Layer (Unit Tests) — test use cases in isolation, mock all dependencies
- Infrastructure Layer (Integration Tests) — test adapters with real dependencies
- Presentation Layer (Unit Tests) — test presenters/view models, mock use cases
Test Double Vocabulary:
- Stubs: Provide canned answers
- Spies: Record calls for verification
- Mocks: Verify behavior expectations
- Fakes: Working implementations for testing
Testing Strategies:
- Test behavior, not implementation details
- Use native async test primitives; avoid manual callback synchronization when the language supports
await - Factory helper pattern: create the system under test (SUT) and its dependencies together in a
makeSUT()helper to keep individual test methods clean - Arrange, Act, Assert structure; extract helpers for clarity
Output: Comprehensive testing strategy document
Reference: See references/testing_strategies.md for patterns and best practices
Step 5: Document Decisions
Objective: Create clear architectural documentation and decision records
ADR Format:
# ADR-NNN: <Decision Title>
## Status: Accepted | Superseded | Deprecated
## Context: Why this decision was needed
## Decision: What was decided
## Consequences: Positive and negative outcomes
## Alternatives Considered: What was rejected and why
See examples/generic/ for a complete ADR example.
Documentation Requirements:
- Architecture Overview — system context diagram, component diagram, layer relationships
- Component Documentation — purpose, dependencies, usage examples
- Design Patterns Used — which patterns and why, trade-offs
- Testing Strategy — what gets tested and how
Output: Complete architectural documentation
Reference: See references/ for detailed documentation patterns
Best Practices
DO ✅
- Separate business logic from infrastructure
- Depend on abstractions, not concretions
- Make dependencies explicit through initializer injection
- Write tests for all business logic
- Use async IO protocols at network/network boundaries; synchronous protocols for in-process storage
- Isolate presentation types to the main/UI thread using the language's concurrency primitives
- Design for testability from the start
DON'T ❌
- Let business logic depend on frameworks
- Use singletons for dependency management
- Mix presentation and business logic
- Create god classes with multiple responsibilities
- Use manual callback/completion-handler patterns where the language's native async/await is available
- Dispatch to the UI thread manually — use structured concurrency instead
Swift/iOS: Use
async throwsat network boundaries, syncthrowsat cache boundaries,@MainActorfor presentation types, andSendablefor value types crossing actor boundaries. See the Swift Concurrency section below.
Common Architectural Patterns
- Clean Architecture (Recommended) — clear separation of concerns, dependency rule: inward only
- Hexagonal Architecture (Ports & Adapters) — business logic at the center, ports define boundaries
- MVVM — separation of UI and logic, testable view models
- MVC — traditional separation, controller as composition root
Reference: See examples/generic/layered_architecture.md for a language-agnostic layered example; examples/swift/ for MVVM+Coordinator, Composition Root, and Two-Layer View in Swift/iOS
Language-Specific Guidance
Swift/iOS
- Use
async throwsprotocols for network boundaries - Use sync
throwsfor cache/store boundaries; bridge to async viaSchedulerprotocol - Apply
@MainActorto presentation types (ResourceView,LoadResourcePresenter) - Mark domain value types
Sendable(e.g.FeedImage: Hashable, Sendable) - Use
LoadResourcePresenter<Resource, View>generic presenter — one type for all features - Use
WeakRefVirtualProxy<T>to break retain cycles without per-type boilerplate - Apply Composition Root pattern in
SceneDelegate; extract infrastructure into@MainActorservice objects (seeexamples/swift/composition_root.md)
Reference: See examples/swift/ for Swift-specific patterns
Generic/Agnostic
- Apply SOLID principles universally
- Use interfaces/traits/protocols depending on language
- Adapt patterns to language features
- Maintain Clean Architecture layers
Reference: See examples/generic/ for language-agnostic examples
Swift Concurrency (Swift/iOS only)
The concepts in this section — async IO boundaries, main-thread isolation, value-type thread safety, generic presenter, memory-safe proxies — apply universally. The syntax and APIs below are Swift-specific. For the detailed implementations, see
references/concurrency_patterns.md.
Async Protocol Boundaries
Network protocols use async throws; store protocols use sync throws:
// Network boundary — async because IO is inherently async
public protocol HTTPClient {
func get(from url: URL) async throws -> (Data, HTTPURLResponse)
}
// Cache/store boundary — sync because the store runs on its own queue (bridged via Scheduler)
// Apply this pattern to any persistent store: CoreData, SQLite, in-memory, Realm, etc.
public protocol ItemStore {
func delete() throws
func insert(_ items: [LocalItem], timestamp: Date) throws
func retrieve() throws -> CachedItems?
}
// Essential Feed uses FeedStore with identical structure for FeedImage caching
@MainActor Isolation
Presentation layer types are @MainActor — no manual thread dispatching:
@MainActor public protocol ResourceView {
associatedtype ResourceViewModel
func display(_ viewModel: ResourceViewModel)
}
@MainActor public final class LoadResourcePresenter<Resource, View: ResourceView> {
public typealias Mapper = (Resource) throws -> View.ResourceViewModel
public init(resourceView: View, loadingView: ResourceLoadingView,
errorView: ResourceErrorView, mapper: @escaping Mapper)
public init(resourceView: View, loadingView: ResourceLoadingView,
errorView: ResourceErrorView) where Resource == View.ResourceViewModel
public func didStartLoading()
public func didFinishLoading(with resource: Resource)
public func didFinishLoading(with error: Error)
}
Sendable Value Types
Domain entities crossing actor boundaries must be Sendable. All stored properties must themselves be Sendable; the compiler verifies this:
// Generic pattern — apply to any domain entity
public struct Item: Hashable, Sendable {
public let id: UUID
public let title: String
// String, UUID, URL are all Sendable — compiler is satisfied
}
// Concrete example from Essential Feed:
// public struct FeedImage: Hashable, Sendable { ... }
WeakRefVirtualProxy
Prevents retain cycles via a single generic type with conditional conformances:
final class WeakRefVirtualProxy<T: AnyObject> {
private weak var object: T?
init(_ object: T) { self.object = object }
}
extension WeakRefVirtualProxy: ResourceErrorView where T: ResourceErrorView {
func display(_ viewModel: ResourceErrorViewModel) { object?.display(viewModel) }
}
extension WeakRefVirtualProxy: ResourceLoadingView where T: ResourceLoadingView {
func display(_ viewModel: ResourceLoadingViewModel) { object?.display(viewModel) }
}
Scheduler Protocol
Bridges sync FeedStore into async contexts without making the store async:
protocol Scheduler {
@MainActor
func schedule<T>(_ action: @escaping @Sendable () throws -> T) async rethrows -> T
}
Reference: See references/concurrency_patterns.md for full implementations and migration guide
Integration with Requirements Engineering
- Start with requirements → Use Cases → BDD scenarios
- Apply this skill → Architecture → Component design
- Implement → Following architectural patterns
- Test → Using defined testing strategy
References
Inside references/:
- clean_architecture.md - Clean Architecture layers and rules
- solid_principles.md - Detailed SOLID explanations with examples
- design_patterns.md - Common patterns (Adapter, Decorator, Composite, Null Object, etc.)
- null_object_pattern.md - Null Object Pattern with testing examples
- command_query_separation.md - CQS principle for cache design
- dependency_management.md - DI patterns and strategies
- testing_strategies.md - Testing patterns and best practices
- modular_design.md - Module organization and boundaries
- concurrency_patterns.md - Swift Concurrency: async/await, @MainActor, Sendable, Scheduler
Inside examples/:
- swift/ - Real implementations from Essential Feed (Swift 6)
- generic/ - Language-agnostic examples
Output Format
When applying this skill, provide:
- Component Analysis - Identified components and responsibilities
- SOLID Review - Applied principles with rationale
- Architecture Diagram - Layers and dependencies (Mermaid)
- Testing Strategy - Test structure and coverage plan
- ADRs - Key decisions documented
- Implementation Guide - Step-by-step refactoring or implementation plan
Credits
Based on the Essential Developer's proven architecture methodology:
Version History
- 2.1.0 - Generalized for multi-project use: domain-neutral SOLID examples, separated Swift-specific guidance, added scope labels, created generic ADR template and updated generic architecture example
- 2.0.0 - Swift 6 migration: async/await, @MainActor, Sendable, LoadResourcePresenter, Scheduler, WeakRefVirtualProxy
- 1.0.0 - Initial release with 5-step process