clean-architecture
Structure software around the Dependency Rule: source code dependencies point inward from frameworks to use cases to entities. Use when the user mentions "architecture layers", "dependency rule", "ports and adapters (hexagonal)", "onion architecture", "screaming architecture", "w
Install
npx skills add https://github.com/wondelai/skills/tree/main/plugins/wondelai-skills/skills/clean-architecture
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wondelai-skills@llmmart
git clone https://github.com/wondelai/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wondelai/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Clean Architecture Framework
A disciplined approach to structuring software so that business rules remain independent of frameworks, databases, and delivery mechanisms. Apply these principles when designing system architecture, reviewing module boundaries, or advising on dependency management.
Core Principle
Source code dependencies must point inward — toward higher-level policies. Nothing in an inner circle can know anything about an outer circle. This single rule produces systems that are testable and independent of frameworks, UI, database, and any external agency. Business rules are what matter; databases, web frameworks, and delivery mechanisms are details — when details depend on policies, you can defer decisions, swap implementations, and test business logic in isolation.
Scoring
Goal: 10/10. Score one point for each of the seven Quick Diagnostic rows the architecture satisfies (0-7), then map to a 0-10 band: 6-7 satisfied = 9-10 (Dependency Rule holds, business logic is framework- and DB-independent); 4-5 = 6-8 (core is testable but some details leak inward); 2-3 = 3-5 (framework or persistence dictates structure); 0-1 = 0-2 (no boundaries — business rules live in controllers and ORM models). Report the score, the failed diagnostic rows, and the specific inversion needed to fix each.
1. Dependency Rule and Concentric Circles
Core concept: Organize the architecture as concentric circles — Entities (enterprise business rules) innermost, then Use Cases (application business rules), then Interface Adapters, with Frameworks and Drivers outermost. Source code dependencies always point inward.
Why it works: When high-level policies don't depend on low-level details, you can swap the database, web framework, or API style without touching business logic — the system becomes resilient to the most volatile parts of the stack.
Key insights:
- Inner circles cannot mention outer circle names — no classes, functions, variables, or data formats from outside
- Data crossing a boundary must be in the form most convenient for the inner circle, never dictated by the outer
- Dependency Inversion (interfaces defined inward, implemented outward) is the mechanism that enforces the rule
- The number of circles is not fixed — four is typical; the rule stays the same
- Frameworks are details, not architecture — they belong in the outermost circle
Code applications:
| Context | Pattern | Example |
|---|---|---|
| Layer direction | Inner circles define interfaces; outer implement | UserRepository interface in Use Cases; PostgresUserRepository in Adapters |
| Data crossing | DTOs cross boundaries, not ORM entities | Use Case returns UserResponse DTO, not an ActiveRecord model |
| Dependency direction | Import arrows always point inward | Controller imports Use Case; Use Case never imports Controller |
See references/dependency-rule.md when an inner-circle import points outward and you need the four-circle code walkthrough, the data-crossing rules, and the four-step dependency-inversion procedure to fix it.
2. Entities and Use Cases
Core concept: Entities encapsulate enterprise-wide business rules — rules that would exist even without software. Use Cases contain application-specific rules that orchestrate the flow of data to and from Entities.
Why it works: Separating what the business does (Entities) from how the application orchestrates it (Use Cases) lets you reuse Entities across applications and change application behavior without altering core business rules.
Key insights:
- Entities are not database rows — they are objects or pure functions encapsulating critical business rules
- Use Cases accept Request Models and return Response Models — never framework objects
- Each Use Case is a single application operation (
CreateOrder,ApproveExpense) - The Interactor pattern: a Use Case class implements an input boundary interface and calls an output boundary interface
- Changes to a Use Case should never affect an Entity; Entity changes may ripple to Use Cases
Code applications:
| Context | Pattern | Example |
|---|---|---|
| Entity design | Critical business rules, zero framework dependencies | Order.calculateTotal() applies tax rules; knows nothing about HTTP |
| Request/Response | Simple data structures cross the boundary | CreateOrderRequest { items, customerId } — no ORM models |
| Single responsibility | One Use Case per operation | PlaceOrder, CancelOrder, RefundOrder as separate classes |
| Interactor | Implements Input Port, calls Output Port | PlaceOrderInteractor implements PlaceOrderInput |
See references/entities-use-cases.md when designing an Interactor or deciding what belongs in an Entity versus a Use Case — full Enterprise vs. Application Business Rules treatment with request/response model examples.
3. Interface Adapters and Frameworks
Core concept: Interface Adapters convert data between the form convenient for Use Cases/Entities and the form required by external agencies. Frameworks and Drivers are the outermost layer — glue code to the outside world.
Why it works: When the web framework, ORM, or message queue is confined to the outer circles, replacing any of them is a localized change. The database is a detail; the web is a detail; details should be plugins to your business rules, not the skeleton of the application.
Key insights:
- Controllers translate HTTP into Use Case input; Presenters translate Use Case output into view models
- Gateways implement repository interfaces defined by Use Cases — the inner circle defines the contract, the outer fulfills it
- Business rules never know whether data lives in SQL, NoSQL, or flat files, or that delivery is HTTP
- Treat frameworks with suspicion — they want you to couple to them; keep them at arm's length
Code applications:
| Context | Pattern | Example |
|---|---|---|
| Controller | Delivery mechanism → Use Case input | OrderController.create(req) builds CreateOrderRequest, calls Interactor |
| Presenter | Use Case output → view model | OrderPresenter.present(response) formats for JSON/HTML |
| Gateway | Repository interface implemented per DB | SqlOrderRepository implements OrderRepository |
| Framework boundary | Framework calls inward, never the reverse | Express route handler calls Controller; Controller never imports Express |
See references/adapters-frameworks.md when wiring controllers, presenters, or gateways, or arguing that the database/web is a detail — covers plugin architecture and how to confine a framework to the edges.
4. Component Principles
Core concept: Components are the units of deployment. Three cohesion principles govern what goes inside a component; three coupling principles govern relationships between components.
Why it works: Poorly composed components create ripple effects where one change forces redeployment of unrelated code; the principles keep changes localized and releases independent.
Key insights:
- REP (Reuse/Release Equivalence): classes in a component must be versionable and releasable as a unit
- CCP (Common Closure): classes that change for the same reason at the same time belong together — SRP for components
- CRP (Common Reuse): don't force users to depend on classes they don't use
- ADP (Acyclic Dependencies): the component graph must have no cycles — break them with DIP or a new component
- SDP (Stable Dependencies): depend in the direction of stability
- SAP (Stable Abstractions): stable components should be abstract; unstable ones concrete
Code applications:
| Context | Pattern | Example |
|---|---|---|
| Component grouping | Group classes that change together (CCP) | All order-related Use Cases in one component |
| Breaking cycles | Apply DIP to invert a dependency edge | Extract an interface into a new component to break the cycle |
| Stability metrics | Instability I = Ce / (Ca + Ce) | Many incoming, no outgoing deps → I near 0 (stable) |
See references/component-principles.md when grouping classes into deployable components or breaking a dependency cycle — each of REP, CCP, CRP, ADP, SDP, SAP worked through with the instability metric.
5. SOLID Principles
Core concept: Five class-and-module-level principles — Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion — the mid-level building blocks that make the Dependency Rule possible.
Why it works: Each principle addresses a specific way dependencies go wrong, preventing the rigidity, fragility, and immobility that turn codebases into legacy nightmares.
Key insights:
- SRP: a module has one reason to change — it serves one actor (not "does one thing")
- OCP: extend behavior by adding new code, not modifying existing code — strategy and plugin patterns
- LSP: subtypes must be usable through the base interface without the client knowing — violated by unexpected exceptions or ignored methods
- ISP: clients should not depend on methods they don't use — fat interfaces create needless coupling
- DIP: high-level modules and low-level modules both depend on abstractions defined by the high-level module
Code applications:
| Context | Pattern | Example |
|---|---|---|
| SRP violation | Class serves multiple actors | Employee handles pay (CFO), reporting (COO), persistence (CTO) |
| OCP via strategy | New behavior through new classes | Add ExpressShipping implementing ShippingStrategy; Order untouched |
| LSP violation | Subtype changes expected behavior | Square extends Rectangle breaks the setWidth()/setHeight() contract |
| ISP application | Split fat interfaces into role interfaces | Printer, Scanner, Fax instead of one MultiFunctionDevice |
| DIP wiring | High-level defines interface; low-level implements | OrderService depends on PaymentGateway, not StripeClient |
See references/solid-principles.md when applying SRP/OCP/LSP/ISP/DIP to a specific class or diagnosing a violation — each principle worked through with code examples and the smell it prevents.
6. Boundaries and Boundary Anatomy
Core concept: A boundary is a line between things that matter and things that are details, implemented through polymorphism: dependencies cross pointing inward while control flow may cross either way.
Why it works: Every boundary buys the option to defer a decision or swap an implementation; strategic boundary placement determines whether a system is a joy or a pain to maintain over years.
Key insights:
- Full boundaries use reciprocal interfaces on both sides; partial boundaries use a simpler strategy or facade
- Humble Object pattern: split boundary code into a hard-to-test part (close to the boundary) and an easy-to-test part (the logic)
- Services are not automatically architectural boundaries — a microservice with a fat shared data model is a monolith with network calls
- Tests are the most isolated component: they depend inward, nothing depends on them
- Premature boundaries are expensive, but so are missing ones — draw them at points of likely volatility
Code applications:
| Context | Pattern | Example |
|---|---|---|
| Full vs. partial boundary | Reciprocal ports, or a lone strategy | Use Case defines PlaceOrderInput/PlaceOrderOutput; simpler cases take a ShippingStrategy |
| Humble Object | Separate testable logic from infrastructure | PresenterLogic (testable) produces ViewModel; View (humble) renders it |
| Main as plugin | Composition root assembles the system | main() wires all concrete implementations and starts the app |
See references/boundaries.md when deciding where to draw a boundary, choosing full vs. partial, or applying the Humble Object pattern — also covers services as boundaries, test boundaries, and Main as the ultimate plugin.
Common Mistakes
| Mistake | Why It Fails | Fix |
|---|---|---|
| ORM leaking into business logic | Entities couple to the schema; DB changes rewrite business rules | Separate domain entities from persistence models; map at the adapter layer |
| Business rules in controllers | Untestable without HTTP; duplicated across endpoints | Move logic into Use Case Interactors; controllers only translate and delegate |
| Framework-first architecture | Framework dictates structure; swapping means a rewrite | Treat the framework as a plugin; structure code by business capability |
| Circular component dependencies | Changes ripple unpredictably; no independent releases | Apply DIP or extract a shared abstraction component |
| One giant Use Case per feature | Bloated thousand-line orchestrators | Split into focused single-operation Use Cases |
| Skipping boundaries "because it's simple" | Coupling accumulates silently until the cost is enormous | Draw boundaries proactively at points of likely volatility |
| Microservices as automatic good architecture | A distributed monolith is worse than a clean monolith | Apply the Dependency Rule within and across services; services are deployment boundaries, not architectural ones |
Quick Diagnostic
| Question | If No | Action |
|---|---|---|
| Can you test business rules without DB, web server, or framework? | Rules coupled to infrastructure | Extract entities and use cases behind interfaces; mock outer layers |
| Do all source dependencies point inward? | Dependency Rule violated | Introduce boundary interfaces; invert the offending dependency |
| Can you swap the database without touching business logic? | Persistence leaking inward | Repository pattern; isolate persistence in adapters |
| Are Use Cases independent of delivery mechanism? | Use Cases know HTTP/CLI/queues | Use plain DTOs in Use Case signatures |
| Is the framework confined to the outermost circle? | Framework is your architecture | Wrap framework calls behind interfaces; push to the edges |
| Is the component graph cycle-free? | Circular dependencies exist | Apply ADP: DIP or new components to break every cycle |
| Does Main (composition root) wire all dependencies? | Concrete classes instantiated in inner circles | Move construction to Main; use DI or factories |
Further Reading
Based on Robert C. Martin's definitive guide to software architecture:
- "Clean Architecture: A Craftsman's Guide to Software Structure and Design" by Robert C. Martin
About the Author
Robert C. Martin ("Uncle Bob") is a software engineer programming since 1970, a founding signatory of the Agile Manifesto, and the author of Clean Code, The Clean Coder, Clean Architecture, and Clean Agile. His SOLID principles are foundational vocabulary in object-oriented design, and his work argues that architecture is about managing dependencies and keeping business rules independent of infrastructure details.
Files (skills)
-
references
-
adapters-frameworks.md 15.5 KB
# Interface Adapters and Frameworks Interface Adapters and Frameworks & Drivers form the two outermost circles of Clean Architecture. Interface Adapters translate data between the forms convenient for Use Cases and Entities and the forms convenient for external agencies. Frameworks and Drivers are the glue code that connects the system to the outside world. Together, these layers contain all the volatile, technology-specific decisions -- the parts most likely to change over the life of a system. This reference covers controllers, presenters, gateways, the nature of frameworks as details, database and web as details, keeping frameworks at arm's length, and the plugin architecture. ## Table of Contents 1. [Interface Adapters](#interface-adapters) 2. [Frameworks as Details](#frameworks-as-details) 3. [The Database Is a Detail](#the-database-is-a-detail) 4. [The Web Is a Detail](#the-web-is-a-detail) 5. [Plugin Architecture](#plugin-architecture) 6. [Keeping Frameworks at Arm's Length](#keeping-frameworks-at-arms-length) --- ## Interface Adapters ### Controllers A Controller is an adapter that translates input from the delivery mechanism (HTTP, CLI, message queue, gRPC) into a form that the Use Case can understand. It constructs a Request Model and calls the Use Case's Input Port. **Responsibilities of a Controller:** - Parse and extract data from the delivery mechanism's native format - Construct the Use Case's Request Model - Call the Use Case's Input Port - Handle delivery-mechanism-specific concerns (authentication, rate limiting) BEFORE calling the Use Case **What a Controller must NOT do:** - Contain business logic - Directly access the database - Format output for the response (that's the Presenter's job) - Know about other controllers ```python # Controller in the Adapters circle class OrderController: def __init__(self, place_order: PlaceOrderInput): self._place_order = place_order def create(self, http_request: dict) -> None: # Translate HTTP data to Use Case request request = PlaceOrderRequest( customer_id=http_request["customer_id"], items=[ OrderItemRequest( product_id=item["product_id"], quantity=item["quantity"], unit_price=item["unit_price"], ) for item in http_request["items"] ], shipping_address=AddressRequest( street=http_request["address"]["street"], city=http_request["address"]["city"], zip_code=http_request["address"]["zip"], ), ) # Delegate to the Use Case self._place_order.execute(request) ``` The Controller knows about HTTP data format and knows about `PlaceOrderRequest`. It translates between the two. The Use Case never sees HTTP. ### Presenters A Presenter translates Use Case output into a form suitable for the delivery mechanism. It implements the Use Case's Output Port and produces a View Model. **The Presenter pattern separates two concerns:** 1. The Use Case decides WHAT data to present 2. The Presenter decides HOW to format it for display ```python # Output Port defined in Use Case circle class PlaceOrderOutput(ABC): @abstractmethod def present_success(self, response: OrderResponse) -> None: pass @abstractmethod def present_failure(self, message: str) -> None: pass # Presenter in the Adapters circle class JsonOrderPresenter(PlaceOrderOutput): def __init__(self): self.view_model: dict = {} self.status_code: int = 200 def present_success(self, response: OrderResponse) -> None: self.status_code = 201 self.view_model = { "data": { "id": response.order_id, "total": f"${response.total}", "status": response.status.capitalize(), "estimated_delivery": response.estimated_delivery, } } def present_failure(self, message: str) -> None: self.status_code = 400 self.view_model = {"error": {"message": message}} ``` The Presenter knows about JSON structure, status codes, and string formatting. The Use Case knows nothing about any of this. ### Gateways A Gateway implements a repository or service interface defined by the Use Case circle using a specific technology. It is the adapter between the abstract port and the concrete implementation. ```python # Interface defined in Use Case circle class OrderRepository(ABC): @abstractmethod def save(self, order: Order) -> None: pass @abstractmethod def find_by_id(self, order_id: str) -> Order | None: pass # Gateway in the Adapters circle class PostgresOrderRepository(OrderRepository): def __init__(self, connection_pool): self._pool = connection_pool def save(self, order: Order) -> None: with self._pool.connection() as conn: conn.execute( "INSERT INTO orders (id, customer_id, total, status) VALUES (%s, %s, %s, %s)", (order.id, order.customer_id, str(order.calculate_total()), order.status.value), ) for item in order.items: conn.execute( "INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (%s, %s, %s, %s)", (order.id, item.product_id, item.quantity, str(item.price)), ) def find_by_id(self, order_id: str) -> Order | None: with self._pool.connection() as conn: row = conn.execute("SELECT * FROM orders WHERE id = %s", (order_id,)).fetchone() if row is None: return None items = conn.execute("SELECT * FROM order_items WHERE order_id = %s", (order_id,)).fetchall() return self._to_domain(row, items) def _to_domain(self, row, item_rows) -> Order: # Map database rows back to domain entity items = [OrderItem(r["product_id"], r["quantity"], Money(r["price"])) for r in item_rows] return Order(order_id=row["id"], items=items, customer_id=row["customer_id"]) ``` Notice the `_to_domain` method: it maps between the persistence format (database rows) and the domain format (entity objects). This mapping is the gateway's core responsibility. ### Adapter Types Summary | Adapter | Translates From | Translates To | Direction | |---------|----------------|---------------|-----------| | **Controller** | External input (HTTP, CLI, event) | Use Case Request Model | Inward | | **Presenter** | Use Case Response Model | View Model (JSON, HTML, CLI output) | Outward | | **Gateway** | Repository/Service Interface | Concrete technology (SQL, API, file) | Outward | | **Mapper** | Domain Entity | Persistence Model (ORM, document) | Both directions | ## Frameworks as Details ### The Framework Trap Frameworks are powerful tools. They provide routing, dependency injection, ORM, template rendering, and dozens of other features. The temptation is to build your system on top of the framework -- to let the framework be the architecture. This is a trap. When the framework IS the architecture: - You cannot test business logic without the framework running - You cannot change the framework without rewriting the application - Framework bugs become your bugs, in your most critical code - Framework upgrades force changes throughout the system - Your code becomes an accessory to the framework rather than the framework serving your code ### Frameworks Want Marriage, You Want a Fling Frameworks are authored by people who have a use case for them. They provide massive power and convenience -- but they ask for commitment. They want you to: - Inherit from their base classes - Put their annotations on your code - Store your data in their preferred format - Structure your project their way Each of these is a coupling point. The more you comply, the harder it is to separate. **The Clean Architecture approach:** - Don't derive business objects from framework base classes - Don't put framework annotations on domain entities - Don't let the framework dictate your project structure - Treat the framework as a tool in the outermost circle, not as the foundation ### Practical Framework Isolation | Framework Feature | Coupled Approach | Decoupled Approach | |-------------------|-----------------|-------------------| | **Routing** | Business logic in route handlers | Route handlers call Controllers; Controllers call Use Cases | | **ORM** | Domain entities ARE ORM models | Separate domain entities; map to/from ORM models in gateways | | **Validation** | Framework validation decorators on entities | Validation in Use Case or domain layer using plain code | | **Dependency injection** | `@Inject` annotations on domain classes | Constructor injection; wiring in Main component | | **Configuration** | `Settings.get("key")` in business logic | Inject config values as constructor parameters | | **Logging** | Framework logger called directly in Use Cases | Inject a logger interface; implement with framework in outer circle | ## The Database Is a Detail The database is a detail. It is a mechanism for storing and retrieving data. From the perspective of the business rules, it doesn't matter whether data lives in PostgreSQL, MongoDB, flat files, or an in-memory data structure. ### Why It Matters When business rules know about the database: - Testing requires a database (slow, fragile tests) - Database schema changes ripple into business logic - Migrating to a different database means rewriting business rules - The data model is driven by database capabilities rather than business needs ### Repository Pattern The repository pattern is the primary mechanism for keeping the database at arm's length: 1. **Define the interface in the Use Case circle** -- it describes WHAT operations the business needs, not HOW data is stored 2. **Implement the interface in the Adapter circle** -- this is where SQL, ORM calls, and database-specific code live 3. **Inject the implementation at startup** -- Main wires the concrete repository into the use case ### ORM Considerations ORMs are useful tools, but they must be contained in the outer circles: **The two-model approach:** - **Domain model**: Pure business entities with business methods and rules. No ORM annotations. Lives in the Entity circle. - **Persistence model**: ORM-annotated classes that map to database tables. Lives in the Adapter circle. The gateway maps between the two. This duplication is intentional and valuable. The domain model evolves with business rules; the persistence model evolves with the database schema. They change for different reasons at different times. ## The Web Is a Detail The web is a delivery mechanism -- a way to transport data between the user and the application. The business rules should not know whether they are being accessed through a web browser, a mobile app, a CLI, or a message queue. ### Delivery Mechanism Independence When use cases are independent of the delivery mechanism, you can: - Serve the same business logic through REST, GraphQL, gRPC, CLI, and WebSocket simultaneously - Test business logic without HTTP - Migrate from one web framework to another by rewriting only the outer circle ### Multiple Delivery Mechanisms ``` REST Controller ----\ \ GraphQL Resolver ------> Use Case Interactor ---> Entity / CLI Command --------/ Message Handler ---/ ``` Each delivery mechanism is an adapter in the outer circle. They all call the same Use Case Input Port. The business logic is written once and exposed through as many delivery mechanisms as needed. ## Plugin Architecture The ultimate expression of Clean Architecture is the plugin architecture: the business rules are the core application, and everything else (database, web framework, external services, UI) is a plugin that connects to the core. ### How Plugins Work 1. **The core defines interfaces** (ports) that describe what it needs from the outside world 2. **Plugins implement those interfaces** using specific technologies 3. **Main assembles the plugins** and injects them into the core at startup 4. **The core never knows which plugins are attached** -- it only knows the interfaces ### The Main Component Main is the dirtiest, most concrete component in the system. It knows about everything because it must instantiate and wire all the pieces together. But nothing depends on Main. ```python # main.py -- the composition root def create_app(): # Concrete infrastructure db_pool = create_connection_pool(os.environ["DATABASE_URL"]) email_client = SendGridClient(os.environ["SENDGRID_API_KEY"]) # Gateways (implement interfaces) order_repo = PostgresOrderRepository(db_pool) email_service = SendGridEmailService(email_client) # Presenters order_presenter = JsonOrderPresenter() # Use Cases (wired with concrete dependencies) place_order = PlaceOrderInteractor(order_repo, order_presenter) cancel_order = CancelOrderInteractor(order_repo, email_service, order_presenter) # Controllers (wired with use cases) order_controller = OrderController(place_order, cancel_order) # Framework wiring app = Flask(__name__) app.route("/orders", methods=["POST"])(order_controller.create) app.route("/orders/<id>/cancel", methods=["POST"])(order_controller.cancel) return app ``` Main is the only place where the concrete classes from all circles come together. If you want to swap PostgreSQL for DynamoDB, you change Main and add a `DynamoOrderRepository`. No other file changes. ### Plugin Swappability in Practice | Plugin | Interface | Implementation A | Implementation B | |--------|-----------|-----------------|-----------------| | **Persistence** | `OrderRepository` | `PostgresOrderRepository` | `DynamoOrderRepository` | | **Email** | `EmailService` | `SendGridEmailService` | `SesEmailService` | | **Payment** | `PaymentGateway` | `StripeGateway` | `BraintreeGateway` | | **Search** | `ProductSearch` | `ElasticsearchProductSearch` | `AlgoliaProductSearch` | | **Cache** | `CacheStore` | `RedisCacheStore` | `MemcachedCacheStore` | | **File storage** | `FileStore` | `S3FileStore` | `LocalFileStore` | Each swap is a single line change in Main plus a new implementation class. No business logic changes. No use case changes. No entity changes. This is the power of treating frameworks and infrastructure as plugins. ## Keeping Frameworks at Arm's Length ### The Wrapper Strategy When a framework provides something useful but you don't want to couple to it directly, wrap it: ```python # Interface in inner circle class Clock(ABC): @abstractmethod def now(self) -> datetime: pass # Wrapper in outer circle class SystemClock(Clock): def now(self) -> datetime: return datetime.utcnow() # Test double class FakeClock(Clock): def __init__(self, fixed_time: datetime): self._time = fixed_time def now(self) -> datetime: return self._time ``` Now your business logic depends on `Clock` (an interface you control), not on `datetime.utcnow()` (a library call you don't control). You can test time-dependent logic deterministically. ### When NOT to Wrap Not everything needs a wrapper. Apply the rule pragmatically: - **Standard library types** (strings, lists, dates as data): Don't wrap. They are stable and ubiquitous. - **Utility functions with no side effects**: Don't wrap `math.ceil()` or `json.dumps()`. - **Anything with I/O or side effects** (database, network, filesystem, clock, random): Wrap it. - **Anything from a framework you might swap**: Wrap it. The test is: "Would I need to mock this in a test?" If yes, wrap it behind an interface. -
boundaries.md 17.9 KB
# Boundaries and Boundary Anatomy Boundaries are the lines that separate software elements. In Clean Architecture, boundaries separate policies from details, stable code from volatile code, and high-level concerns from low-level mechanisms. How you draw boundaries, where you place them, and how you implement them determines whether a system remains maintainable over decades or degrades into an unmaintainable monolith. This reference covers boundary anatomy, boundary crossing mechanisms, the Humble Object pattern, partial boundaries, layers and boundaries, services as boundaries, test boundaries, and the Main component as the ultimate plugin. ## Table of Contents 1. [Boundary Anatomy](#boundary-anatomy) 2. [Boundary Crossing](#boundary-crossing) 3. [The Humble Object Pattern](#the-humble-object-pattern) 4. [Partial Boundaries](#partial-boundaries) 5. [Services as Boundaries](#services-as-boundaries) 6. [Test Boundaries](#test-boundaries) 7. [The Main Component as a Plugin](#the-main-component-as-a-plugin) --- ## Boundary Anatomy ### What Is a Boundary? A boundary is a separation between two groups of code where one side should not know about the other. At its core, a boundary is an interface plus a dependency inversion: the inner side defines an abstraction, and the outer side provides a concrete implementation. ### The Structure of a Full Boundary A full boundary has components on both sides, connected through polymorphism: ``` [Client Side] [Boundary] [Implementation Side] | Controller ----calls----> InputPort (interface) | Interactor (implements InputPort) | Interactor ----calls----> OutputPort (interface) | Presenter (implements OutputPort) ``` **Both interfaces are defined on the inner side.** The Controller depends on `InputPort` (inward). The Presenter implements `OutputPort` (inward). The Interactor knows about neither the Controller nor the Presenter directly. ### Boundary Components | Component | Circle | Role | |-----------|--------|------| | **Input Port** | Use Case | Interface that defines what the use case accepts | | **Output Port** | Use Case | Interface that defines what the use case produces | | **Interactor** | Use Case | Implements Input Port; calls Output Port | | **Controller** | Adapter | Calls Input Port; translates from delivery mechanism | | **Presenter** | Adapter | Implements Output Port; translates to display format | | **Data Transfer Objects** | Use Case | Simple structures that carry data across the boundary | | **Gateway Interface** | Use Case | Abstraction for data persistence or external services | | **Gateway Implementation** | Adapter | Concrete persistence or service access | ## Boundary Crossing ### How Data Flows Across Boundaries Data crosses boundaries as simple data structures -- DTOs, structs, or primitives. Never as framework objects, ORM entities, or complex objects that carry dependencies. **Inbound crossing (Controller to Use Case):** ```python # Controller creates a simple DTO and passes it inward @dataclass(frozen=True) class TransferFundsRequest: source_account_id: str destination_account_id: str amount: str # String to avoid float precision issues currency: str # Controller class TransferController: def handle(self, http_body: dict) -> None: request = TransferFundsRequest( source_account_id=http_body["from"], destination_account_id=http_body["to"], amount=http_body["amount"], currency=http_body["currency"], ) self._transfer_use_case.execute(request) ``` **Outbound crossing (Use Case to Presenter):** ```python # Use Case creates a response DTO and passes it outward through the Output Port @dataclass(frozen=True) class TransferFundsResponse: transfer_id: str new_source_balance: str timestamp: str # In the Interactor: response = TransferFundsResponse( transfer_id=transfer.id, new_source_balance=str(source_account.balance), timestamp=transfer.created_at.isoformat(), ) self._presenter.present_success(response) ``` ### Flow of Control vs. Direction of Dependency This is a subtle but critical distinction: - **Flow of control:** Controller --> Interactor --> Presenter (left to right, outward at the end) - **Source code dependency:** Controller --> InputPort <-- Interactor --> OutputPort <-- Presenter The dependencies point inward on both sides of the Interactor. Control flows outward to the Presenter, but the dependency is inverted: the Presenter depends on (implements) an interface defined by the Use Case. ## The Humble Object Pattern ### The Problem Some code is inherently hard to test because it's close to a boundary with something difficult to control -- a GUI, a database connection, a network socket. The Humble Object pattern splits such code into two parts: 1. **The Humble Object:** Contains the hard-to-test code, stripped of all logic. It's so simple that testing is unnecessary (or trivially easy). 2. **The Testable Object:** Contains all the logic, extracted from the hard-to-test context so it can be tested in isolation. ### Pattern Structure ``` [Testable Logic] [Humble Object] PresenterLogic -produces-> ViewModel (easy to test) (simple data) | v View/Template (hard to test, but so simple it doesn't matter) ``` ### Examples of Humble Objects **1. View (GUI boundary):** ```python # Testable: Presenter that produces a ViewModel class OrderPresenterLogic: def present(self, response: OrderResponse) -> OrderViewModel: return OrderViewModel( title=f"Order #{response.order_id}", total=f"${response.total:.2f}", status_color="green" if response.status == "completed" else "yellow", items=[f"{i.name} x{i.qty}" for i in response.items], ) # Humble: View that just renders the ViewModel (no logic to test) class OrderView: def render(self, vm: OrderViewModel) -> str: return self._template.render(vm) # Template rendering only ``` The Presenter is easily testable -- give it a response, assert the ViewModel. The View is humble -- it just passes the ViewModel to a template engine. No logic, no decisions. **2. Database Gateway (persistence boundary):** ```python # Testable: Use Case logic that decides what to persist class ApproveExpenseInteractor: def execute(self, request: ApproveExpenseRequest) -> None: expense = self._repo.find_by_id(request.expense_id) expense.approve(request.approver_id) # Business logic -- testable self._repo.save(expense) # Humble: Repository that just maps and persists (minimal logic) class SqlExpenseRepository: def save(self, expense: Expense) -> None: self._conn.execute( "UPDATE expenses SET status = %s, approved_by = %s WHERE id = %s", (expense.status.value, expense.approver_id, expense.id), ) ``` The Interactor contains the decision logic (testable with a mock repo). The Repository is humble -- it just maps entity state to SQL parameters. **3. Service Gateway (external service boundary):** ```python # Testable: Logic that decides whether and how to send notifications class NotificationService: def __init__(self, sender: NotificationSender): self._sender = sender def notify_order_shipped(self, order: Order) -> None: if order.customer_prefers_email(): self._sender.send_email( to=order.customer_email, subject=f"Order {order.id} shipped", body=self._format_shipping_message(order), ) # Humble: Just sends the message (hard to test, but no logic) class SmtpNotificationSender(NotificationSender): def send_email(self, to: str, subject: str, body: str) -> None: self._smtp.sendmail(self._from_addr, to, self._build_mime(subject, body)) ``` ### Where Humble Objects Appear in Clean Architecture | Boundary | Humble Object | Testable Partner | |----------|--------------|-----------------| | GUI/View | Template renderer, React component | Presenter logic that produces ViewModel | | Database | SQL execution, ORM save/load | Use Case logic, mapping logic | | External API | HTTP client wrapper | Service logic that decides what to send | | Filesystem | File read/write operations | Logic that decides what to read/write | | Clock/Random | System clock, random generator | Logic that uses injected clock/random | ## Partial Boundaries ### When Full Boundaries Are Too Expensive Full boundaries require interfaces on both sides (Input Port and Output Port), separate DTOs, and careful dependency management. Sometimes the anticipated need for a boundary doesn't justify the cost. In these cases, use a partial boundary. ### Three Forms of Partial Boundaries **1. Skip the last step (prepare for full boundary later):** Create the interfaces and separate the components, but deploy them together in the same package. You've done the intellectual work of separation but deferred the deployment separation. ```python # Same package, but clearly separated with interfaces # Can be split into separate packages later with minimal effort class OrderService: def __init__(self, repo: OrderRepository): # Interface exists self._repo = repo class InMemoryOrderRepository(OrderRepository): # Implementation exists ... # Both live in the same package for now ``` **2. Strategy pattern (one-sided boundary):** ```python # Only the outbound side has an interface class ReportGenerator: def __init__(self, formatter: ReportFormatter): self._formatter = formatter def generate(self, data: ReportData) -> str: # Logic here return self._formatter.format(processed_data) class PdfFormatter(ReportFormatter): def format(self, data) -> str: ... class CsvFormatter(ReportFormatter): def format(self, data) -> str: ... ``` No Input Port, no Output Port -- just a simple strategy. Lighter weight than a full boundary. **3. Facade pattern (simplest):** ```python class OrderFacade: """Single entry point to order subsystem. Hides internal complexity.""" def place_order(self, items, customer_id): # Delegates to internal classes order = self._order_factory.create(items, customer_id) self._order_repo.save(order) self._notifier.notify(order) ``` The Facade provides a simpler interface but doesn't enforce dependency direction. It's the weakest form of boundary -- better than nothing, but easily violated. ### Choosing Boundary Strength | Situation | Boundary Type | Cost | Protection | |-----------|--------------|------|------------| | Will definitely need to swap implementations | Full boundary (ports on both sides) | High | Complete | | Might need to swap; want the option | Partial (interfaces, same package) | Medium | Good | | Multiple strategies but stable architecture | Strategy pattern | Low-medium | Moderate | | Just want to simplify access to a subsystem | Facade | Low | Minimal | | Uncertain -- need might never arise | None (but document the decision) | Zero | None | ## Services as Boundaries ### Services Are Not Inherently Architectural A common misconception is that splitting a system into microservices automatically creates clean architectural boundaries. It does not. A microservice with a fat shared database or a shared data model is just a distributed monolith -- all the coupling of a monolith plus the complexity of network communication. ### When Services Create Real Boundaries A service creates a genuine architectural boundary when: - It has its own data store that no other service accesses directly - It communicates through well-defined interfaces (API contracts) - Its internal structure follows the Dependency Rule independently - It can be developed, deployed, and scaled independently ### When Services Fail as Boundaries | Anti-Pattern | Why It Fails | |-------------|-------------| | Shared database | Changes to the schema affect all services -- they're coupled | | Shared data model library | All services import the same DTOs -- they change together | | Synchronous orchestration | Service A calls B calls C calls D -- distributed monolith | | Chatty communication | Services exchange many small calls -- performance and coupling | ### Services Should Contain Clean Architecture Each service should have its own concentric circles internally: ``` Service Boundary ├── Entities (domain objects for this service's bounded context) ├── Use Cases (application logic for this service) ├── Adapters (controllers, gateways, presenters for this service) └── Frameworks (HTTP server, database driver for this service) ``` The service boundary is a deployment boundary. The Clean Architecture circles within each service are architectural boundaries. Both are needed. ## Test Boundaries ### Tests as the Most Isolated Component Tests are the most decoupled component in any system. They depend on the code being tested, but nothing in the production system depends on the tests. Tests always point inward -- they test entities, use cases, and adapters, but no production code imports test code. ### The Testing Boundary Structure ``` [Production Code] [Test Code] Entity ---------<depends-on------ EntityTest UseCase --------<depends-on------ UseCaseTest Adapter --------<depends-on------ AdapterTest (No arrow from Production to Test) ``` ### Testing Each Circle | Circle | Test Strategy | Dependencies Needed | |--------|--------------|-------------------| | **Entities** | Unit tests with no mocks | None -- entities are self-contained | | **Use Cases** | Unit tests with mocked ports | Mock repositories, mock presenters | | **Adapters** | Integration tests | Real database (testcontainers), real HTTP | | **Frameworks** | End-to-end tests | Full system running | ### The Fragile Test Problem When tests depend on implementation details (private methods, internal data structures, specific framework behavior), they break when the code is refactored even though behavior hasn't changed. The Dependency Rule helps: tests should depend on the same interfaces that the production code depends on. ```python # FRAGILE: Test depends on internal implementation def test_order_internal_state(): order = Order(items) assert order._internal_state == "pending" # Private field -- fragile # ROBUST: Test depends on public behavior (same interface as production code) def test_order_is_pending_after_creation(): order = Order(items) assert order.status == OrderStatus.PENDING # Public behavior -- stable ``` ## The Main Component as a Plugin ### Main Is the Dirtiest Component The Main component (or composition root) is the one place where all concrete classes from all circles are known. It creates the concrete instances, wires them together, and starts the system. It is the most concrete, most dependent, and most volatile component. **But nothing depends on Main.** It sits at the outermost edge of the system. It is a plugin to the application -- a configuration detail that determines which concrete implementations are used for each abstract port. ### Main's Responsibilities 1. **Instantiate concrete infrastructure** (database connections, API clients, caches) 2. **Instantiate concrete adapters** (repositories, presenters, gateways) 3. **Instantiate use case interactors** with injected dependencies 4. **Instantiate controllers** with injected use cases 5. **Configure the framework** (routes, middleware, error handlers) 6. **Start the application** (listen on port, begin event loop) ### Different Mains for Different Configurations Because Main is a plugin, you can have multiple Main configurations: ```python # main_production.py def create_app(): repo = PostgresOrderRepository(production_db_pool) emailer = SendGridEmailer(production_api_key) ... # main_test.py def create_app(): repo = InMemoryOrderRepository() emailer = FakeEmailer() ... # main_local.py def create_app(): repo = SqliteOrderRepository("local.db") emailer = ConsoleEmailer() # Prints to stdout ... ``` The business logic (entities, use cases) is identical across all three. Only the wiring in Main changes. This is the ultimate demonstration that frameworks, databases, and external services are details -- plugins that can be swapped by changing the composition root. ### Main and Dependency Injection Frameworks DI frameworks (Spring, Guice, tsyringe) can help wire dependencies in Main. But be careful: - **Use DI framework annotations ONLY in Main or configuration classes** -- never in entities or use cases - The DI framework is itself a framework detail -- it belongs in the outermost circle - You should be able to wire the entire system manually in a test without the DI framework - If removing the DI framework would require changes to business logic, you've coupled too tightly ### The Plugin Architecture Realized When Main is the only place that knows about concrete implementations, the entire system becomes a plugin architecture: ``` Main (composition root) / | | \ / | | \ PostgresRepo SendGrid Express Stripe | | | | v v v v [OrderRepo] [Emailer] [HTTP] [Payment] (interface) (interface) (route) (interface) \ | | / \ | | / Use Case Interactors | Entities ``` Entities and Use Cases sit at the center, defining what they need through interfaces. Main plugs in the concrete implementations. The business rules don't know or care which database, email provider, web framework, or payment processor is being used. They just work. -
component-principles.md 12.5 KB
# Component Principles Components are the units of deployment -- the smallest entities that can be independently deployed. In Java they are jar files, in Ruby they are gems, in .NET they are DLLs, in JavaScript they are npm packages or bundled modules. Robert C. Martin defines six principles that govern how classes should be grouped into components (cohesion) and how components should relate to each other (coupling). This reference covers the three cohesion principles (REP, CCP, CRP), the three coupling principles (ADP, SDP, SAP), practical application of stability and abstractness metrics, and strategies for managing component dependencies. ## Component Cohesion: What Goes Inside a Component ### REP: The Reuse/Release Equivalence Principle **"The granule of reuse is the granule of release."** Classes and modules that are grouped into a component should be releasable together. If you version and release a component, every class in it should make sense as part of that release. A component should have a cohesive theme -- a reason for being grouped. **Why it matters:** - Users of a component expect that when they upgrade to a new version, all classes in the component have been updated coherently - If a component contains unrelated classes, users are forced to upgrade for changes they don't care about - A component without a coherent theme is difficult to document, understand, and maintain **Practical implications:** - A component named `order-domain` should contain `Order`, `OrderItem`, `OrderStatus`, `OrderPolicy` -- all cohesively related to order business rules - It should NOT also contain `UserPreferences` or `EmailTemplate` just because they happen to be used nearby - When you can't write a one-sentence description of what the component does, it probably violates REP ### CCP: The Common Closure Principle **"Gather into components those classes that change for the same reasons and at the same times. Separate into different components those classes that change at different times and for different reasons."** This is the Single Responsibility Principle applied at the component level. A component should not have multiple reasons to change. **Why it matters:** - When a change in business requirements affects multiple classes, ideally all those classes are in the same component - This means only one component needs to be redeployed rather than many - Minimizes the ripple effect of changes across the deployment landscape **Practical application:** | Change Reason | Group Together | Separate From | |---------------|---------------|---------------| | Order pricing rules change | `OrderCalculator`, `DiscountPolicy`, `TaxCalculator` | `OrderController`, `OrderRepository` | | Database schema changes | `OrderMapper`, `OrderRepository`, `OrderMigration` | `Order`, `OrderCalculator` | | API response format changes | `OrderPresenter`, `OrderSerializer`, `OrderViewModel` | `Order`, `OrderService` | | Authentication rules change | `AuthPolicy`, `TokenValidator`, `SessionManager` | `OrderService`, `PaymentService` | **The key question:** "When this business rule changes, which classes will I need to modify?" Group those classes together. ### CRP: The Common Reuse Principle **"Don't force users of a component to depend on things they don't need."** Classes in a component should be tightly related. If you depend on one class in a component, you should depend on most (ideally all) classes in that component. If you only use one class out of twenty, the component is too broad. **Why it matters:** - When a component changes, all components that depend on it must be revalidated and potentially redeployed - If Component A depends on Component B but only uses one class, changes to unrelated classes in B still force A to be revalidated - Fat components create unnecessary coupling **Practical test:** For each class in a component, ask: "If I remove this class, would users of this component notice?" If they wouldn't, the class may belong elsewhere. ### The Tension Triangle REP, CCP, and CRP are in tension with each other: ``` REP / \ / \ CCP ------- CRP ``` - **REP + CCP** push toward larger components (group things that are released and changed together) - **CRP** pushes toward smaller components (don't include things that aren't used together) - **Early in development:** Favor CCP (minimize redeployment cost as code churns) - **As the system matures:** Shift toward CRP (minimize unnecessary coupling as the system stabilizes) A component's composition typically evolves over time, starting broad (CCP-oriented) and narrowing (CRP-oriented) as the system matures. ## Component Coupling: Relationships Between Components ### ADP: The Acyclic Dependencies Principle **"Allow no cycles in the component dependency graph."** The dependency graph of components must be a Directed Acyclic Graph (DAG). If Component A depends on B, and B depends on C, and C depends on A, you have a cycle -- and the three components are effectively one undivisible monolith. **Why cycles are destructive:** - Cycles make independent release impossible -- you can't release A without releasing B and C - Changes in any component in the cycle potentially affect all others - Build order becomes ambiguous or impossible - Testing requires all components in the cycle to be present **Detecting cycles:** ``` A --> B --> C --> A (CYCLE: A, B, C are effectively one component) A --> B --> C (NO CYCLE: DAG) | ^ v | D --------/ ``` **Breaking cycles -- two strategies:** **Strategy 1: Apply the Dependency Inversion Principle** If B depends on A and A depends on B (cycle), extract an interface: ``` Before (cycle): A <--> B After (no cycle): A --> InterfaceX <-- B ``` A defines `InterfaceX`. B implements it. Now A depends on nothing new; B depends on `InterfaceX` (which lives with A). The cycle is broken. **Strategy 2: Extract a new component** Move the shared dependency into a new component that both A and B depend on: ``` Before (cycle): A <--> B After (no cycle): A --> C <-- B ``` C contains the shared classes. Both A and B depend on C. Neither depends on the other. ### SDP: The Stable Dependencies Principle **"Depend in the direction of stability."** A component should only depend on components that are more stable than it is. Stability here means "difficulty of change" -- a component is stable if many other components depend on it (making it hard to change without breaking things). **Measuring stability:** - **Fan-in (Ca):** Number of classes outside the component that depend on classes inside the component (incoming dependencies) - **Fan-out (Ce):** Number of classes inside the component that depend on classes outside the component (outgoing dependencies) - **Instability (I):** I = Ce / (Ca + Ce), where I ranges from 0 (maximally stable) to 1 (maximally unstable) | Metric Value | Meaning | Implication | |-------------|---------|-------------| | I = 0 | Maximally stable (many dependents, no dependencies) | Hard to change; should be abstract | | I = 1 | Maximally unstable (no dependents, many dependencies) | Easy to change; should be concrete | | I = 0.5 | Balanced | Moderate change risk | **The SDP rule:** If Component A depends on Component B, then I(B) should be less than or equal to I(A). You should depend on things that are harder to change than you are. **Violation example:** If a highly stable component (I=0.1, many dependents) depends on a highly unstable component (I=0.9, few dependents), the unstable component's frequent changes will destabilize all the stable component's dependents. ### SAP: The Stable Abstractions Principle **"A component should be as abstract as it is stable."** Stable components (hard to change) should be abstract (contain mostly interfaces and abstract classes). This way, their stability does not prevent them from being extended. Unstable components (easy to change) should be concrete, containing the implementations that change frequently. **Measuring abstractness:** - **Nc:** Number of classes in the component - **Na:** Number of abstract classes and interfaces in the component - **Abstractness (A):** A = Na / Nc, where A ranges from 0 (fully concrete) to 1 (fully abstract) ### The Main Sequence Plot each component on a graph with Instability (I) on the x-axis and Abstractness (A) on the y-axis. The ideal line runs from (0,1) to (1,0) -- the "Main Sequence." ``` A (Abstractness) 1 | * Zone of Uselessness | \ | \ <-- Main Sequence | \ | \ 0 |____________* Zone of Pain 0 1 I (Instability) ``` **Zone of Pain (I=0, A=0):** Maximally stable AND maximally concrete. Very hard to change but contains no abstractions for extension. Examples: database schemas, concrete utility libraries that everyone depends on. Painful to modify. **Zone of Uselessness (I=1, A=1):** Maximally unstable AND maximally abstract. No one depends on these abstract interfaces. They serve no purpose. Dead code. **The Main Sequence:** Components should fall near the line from (0,1) to (1,0). Stable components should be abstract. Unstable components should be concrete. **Distance from the Main Sequence:** D = |A + I - 1|, where D ranges from 0 (on the line) to ~0.7 (in a zone). Components with high D values warrant investigation. ## Practical Component Design ### Component Mapping to Clean Architecture | Clean Architecture Circle | Stability | Abstractness | Character | |--------------------------|-----------|-------------|-----------| | **Entities** | Very stable (I near 0) | Abstract (interfaces, domain types) | Core business rules; many dependents | | **Use Cases** | Stable (I = 0.2-0.4) | Moderately abstract (ports, interactors) | Application rules; depend on entities | | **Adapters** | Unstable (I = 0.5-0.7) | Concrete (controllers, gateways) | Translation layer; depend on use cases | | **Frameworks** | Very unstable (I near 1) | Concrete (configuration, wiring) | Glue code; depend on everything | ### Versioning Components Independently When components are properly decoupled: - Each can have its own version number - Each can be released on its own schedule - Teams can own components independently - Breaking changes in one component can be managed through interface versioning ### Component Design Workflow 1. **Start with CCP:** Group classes by reason for change. Don't worry about component size. 2. **Apply CRP:** Remove classes that aren't used together. Split components that force unnecessary dependencies. 3. **Check REP:** Ensure each component has a coherent theme and can be meaningfully versioned. 4. **Graph dependencies:** Draw the component dependency graph. Look for cycles. 5. **Break cycles (ADP):** Use DIP or extraction to eliminate every cycle. 6. **Check stability direction (SDP):** Ensure dependencies flow toward stability. 7. **Balance abstraction (SAP):** Make stable components abstract; make unstable components concrete. 8. **Measure D:** Plot components on the I/A graph. Investigate those far from the Main Sequence. ### Common Component Anti-Patterns | Anti-Pattern | Symptom | Fix | |-------------|---------|-----| | **God component** | One component contains everything | Split by CCP: group by reason for change | | **Circular dependencies** | Can't release or build independently | Apply ADP: DIP or extraction | | **Concrete stable component** | Many dependents, no abstractions, painful to change | Apply SAP: extract interfaces, move implementations to unstable components | | **Unstable abstractions** | Abstract component with no dependents | Remove dead abstractions or rethink dependency structure | | **Shotgun releases** | Changing one feature requires releasing five components | Apply CCP: group co-changing classes together | | **Dependency magnet** | One utility component everyone depends on | Split into focused components; apply CRP | ### Dependency Analysis Tools | Language | Tool | Purpose | |----------|------|---------| | Java | JDepend, ArchUnit | Measure component metrics, enforce dependency rules | | JavaScript/TypeScript | Dependency Cruiser, Madge | Visualize and validate module dependencies | | Python | import-linter, pydeps | Enforce import rules, visualize package dependencies | | .NET | NDepend | Component metrics, dependency analysis | | Go | `go vet`, custom linters | Package dependency validation | | General | SonarQube | Cross-language dependency and quality analysis | These tools automate the detection of cycles, stability violations, and components in the Zone of Pain or Zone of Uselessness. Integrate them into CI/CD pipelines to prevent architectural drift. -
dependency-rule.md 13.3 KB
# The Dependency Rule and Concentric Circles The Dependency Rule is the single most important concept in Clean Architecture. It states that source code dependencies can only point inward. Nothing in an inner circle can know anything at all about something in an outer circle. This includes names -- functions, classes, variables, data formats, or any other named software entity declared in an outer circle must not be mentioned by code in an inner circle. This reference covers the concentric circles model, how data crosses boundaries, why direction matters, how frameworks violate the rule, and how to keep the inner circle pure. ## The Concentric Circles Clean Architecture organizes code into concentric circles, each representing a different level of abstraction and policy. The innermost circles contain the highest-level, most general policies. The outermost circles contain the lowest-level, most concrete details. ### Circle 1: Entities (Innermost) Entities encapsulate enterprise-wide business rules. These are the most general, most stable rules in the system. They are the least likely to change when something external changes -- a page navigation change, a security policy change, or a database migration should not affect entities. **Characteristics of well-designed entities:** - They can be simple objects with methods, or they can be a set of data structures and functions - They encapsulate the most critical business rules - They have no dependency on anything in the outer circles - They would exist even if no software system existed (the rules are inherent to the business) - They are the most reusable elements across different applications in the enterprise **Example:** ```python class LoanApplication: def __init__(self, applicant_income: float, requested_amount: float, credit_score: int): self.applicant_income = applicant_income self.requested_amount = requested_amount self.credit_score = credit_score def debt_to_income_ratio(self) -> float: return self.requested_amount / (self.applicant_income * 12) def is_creditworthy(self) -> bool: return self.credit_score >= 680 and self.debt_to_income_ratio() < 0.43 ``` This entity knows nothing about databases, HTTP, or frameworks. It encapsulates the business rule that determines creditworthiness. ### Circle 2: Use Cases Use Cases contain application-specific business rules. They orchestrate the flow of data to and from entities and direct those entities to use their enterprise-wide business rules to achieve the goals of the use case. **Characteristics:** - They define and implement input and output port interfaces - They manipulate entities to achieve application goals - Changes to use cases do not affect entities - Changes to external layers (database, UI) do not affect use cases ### Circle 3: Interface Adapters This circle contains adapters that convert data between the format most convenient for the use cases and entities and the format most convenient for some external agency such as the database or the web. **Contains:** - Controllers (translate inbound requests to use case input) - Presenters (translate use case output to external format) - Gateways (implement repository interfaces using specific technologies) ### Circle 4: Frameworks and Drivers (Outermost) The outermost layer is composed of frameworks and tools -- the database, the web framework, the messaging system. This is where all the details go. The web is a detail. The database is a detail. We keep these things on the outside where they can do little harm. **Contains:** - Web framework (Express, Spring, Django, Rails) - Database engine and ORM - External service clients - Device drivers and I/O ## The Direction of Dependencies The arrow of dependency is not the same as the arrow of control flow. Control flow can go in any direction across a boundary. Source code dependencies, however, must always point inward. ### How Control Flow Opposes Dependency Direction Consider this scenario: a controller needs to call a use case, and the use case needs to call a presenter. The control flows outward (from use case to presenter), but the dependency must point inward (presenter depends on use case, not the other way around). The mechanism is Dependency Inversion: ``` Controller --> [Use Case Input Port] <-- Use Case Interactor --> [Use Case Output Port] <-- Presenter ``` The Use Case defines both the Input Port (which the Controller calls) and the Output Port (which the Presenter implements). The Use Case never knows about the Controller or the Presenter directly. It only knows about the interfaces it defines. ```python # Defined in the Use Case circle class PlaceOrderOutputPort(ABC): @abstractmethod def present_success(self, response: OrderResponse) -> None: pass @abstractmethod def present_failure(self, error: str) -> None: pass # Defined in the Use Case circle class PlaceOrderInteractor: def __init__(self, order_repo: OrderRepository, presenter: PlaceOrderOutputPort): self.order_repo = order_repo self.presenter = presenter def execute(self, request: PlaceOrderRequest) -> None: order = Order.create(request.items, request.customer_id) self.order_repo.save(order) self.presenter.present_success(OrderResponse(order.id, order.total)) # Defined in the Adapters circle -- implements the Use Case's interface class JsonOrderPresenter(PlaceOrderOutputPort): def present_success(self, response: OrderResponse) -> None: self.view_model = {"order_id": response.id, "total": str(response.total)} def present_failure(self, error: str) -> None: self.view_model = {"error": error} ``` The Interactor defines `PlaceOrderOutputPort`. The `JsonOrderPresenter` in the outer circle implements it. The dependency points inward even though control flows outward. ## Data Crossing Boundaries When data crosses a boundary, it is always in the form that is most convenient for the inner circle. The outer circle must adapt its data into the form expected by the inner circle. ### Principle: Inner Circle Dictates Data Format **Wrong -- outer circle format leaking inward:** ```python # Use Case receives a Django request object (framework dependency) class CreateUserInteractor: def execute(self, request: HttpRequest): # VIOLATION: knows about Django data = json.loads(request.body) user = User(name=data['name']) ``` **Right -- inner circle defines its own data structure:** ```python # Use Case defines its own request model @dataclass class CreateUserRequest: name: str email: str class CreateUserInteractor: def execute(self, request: CreateUserRequest): # Pure data structure user = User(name=request.name, email=request.email) ``` The Controller in the outer circle is responsible for translating the HTTP request into the `CreateUserRequest`. ### Crossing Data Patterns | Pattern | When to Use | Example | |---------|-------------|---------| | **Request/Response DTOs** | Standard use case boundaries | `CreateOrderRequest` and `CreateOrderResponse` as plain data classes | | **Primitives** | Simple boundaries with few parameters | `get_user(user_id: str) -> UserResponse` | | **Domain events** | Communicating between bounded contexts | `OrderPlaced(order_id, timestamp)` emitted by inner circle | | **Data maps (dicts)** | Crossing boundaries where type safety is less critical | Acceptable in dynamic languages; prefer typed DTOs in static ones | ### What Must Not Cross Boundaries - **ORM entities or database rows**: These are outer circle artifacts. Never pass an ActiveRecord model into a Use Case. - **Framework request/response objects**: `HttpRequest`, `HttpResponse`, `Request`, `Response` -- all belong in the outer circle. - **Third-party library types**: If your Use Case accepts an `AwsS3Object`, you've coupled business logic to AWS. ## How Frameworks Violate the Dependency Rule Frameworks want to be the center of your universe. They ask you to subclass their base classes, decorate your code with their annotations, and structure your project according to their conventions. Every such demand is a dependency pointing outward-to-inward -- a violation. ### Common Framework Violations | Framework Pattern | Violation | Fix | |-------------------|-----------|-----| | **ORM annotations on entities** | Entity depends on database framework | Separate domain entity from ORM model; map between them | | **Controller base classes** | Business logic inherits framework code | Use composition: controller holds a reference to the interactor | | **Framework-specific return types** | Use Case returns `ResponseEntity` or `JsonResponse` | Return plain DTOs; let the adapter format the response | | **Dependency injection via framework** | Inner circle annotated with `@Inject`, `@Autowired` | Use constructor injection with plain interfaces; wire in Main | | **Validation annotations** | Business validation tied to framework | Validate in the use case using plain code or a domain validator | ### Keeping Frameworks at Arm's Length The key insight is to treat the framework as a plugin, not as your architecture: 1. **Don't derive from framework base classes** in your business logic. If the framework requires inheritance, create a thin adapter that inherits from the framework class and delegates to your clean inner code. 2. **Don't scatter framework annotations** throughout your domain. If you must use annotations for ORM mapping, do so on a separate persistence model that maps to and from your domain entity. 3. **Structure your project by business capability**, not by framework convention. Instead of `controllers/`, `models/`, `services/` (framework-driven), use `orders/`, `payments/`, `shipping/` (domain-driven), each with its own layers inside. ## Keeping the Inner Circle Pure The inner circle is the most valuable part of the system because it contains the rules that make the business money. Protecting it from contamination requires vigilance. ### Purity Checklist - **No imports from outer circles**: Grep your entity and use case code for imports of framework, database, or infrastructure packages. There should be none. - **No I/O**: Inner circle code never reads from a file, queries a database, or makes an HTTP call directly. It calls an interface, and the outer circle provides the implementation. - **No global state or singletons** that come from outer circles: If a use case accesses `Settings.DATABASE_URL`, it depends on infrastructure. - **No concurrency primitives** from the framework: Threads, async runtime, and event loops are outer circle concerns. Use cases should be synchronous-looking; the adapter handles async mechanics. - **Testable in isolation**: If you cannot instantiate a use case with mock implementations and run it without starting any server, database, or framework, the inner circle is not pure. ### Enforcement Strategies | Strategy | How It Works | Tools | |----------|-------------|-------| | **Architecture tests** | Automated tests that verify import rules | ArchUnit (Java), Dependency Cruiser (JS/TS), import-linter (Python) | | **Module boundaries** | Language-level visibility (packages, modules) | Java modules, Go internal packages, Rust `pub(crate)` | | **Build system separation** | Inner and outer circles are separate build targets | Separate Gradle modules, npm packages, or Python packages | | **Code review rules** | Manual review for dependency direction violations | PR checklist: "Do any new imports in the domain cross outward?" | ### The Four-Step Inversion Process When you discover an outward dependency in an inner circle: 1. **Identify the dependency**: What concrete outer-circle class is being referenced? 2. **Define an interface in the inner circle** that describes what the inner circle needs (not what the outer circle provides). 3. **Move the concrete implementation to the outer circle**, implementing the inner circle's interface. 4. **Wire the dependency in Main** (the composition root), injecting the concrete implementation into the inner circle at startup. This process always works. It may feel like ceremony, but it's the mechanism that keeps the most valuable code in your system independent of the most volatile. ## The Dependency Rule in Practice: A Complete Example Consider an e-commerce system handling order placement: ``` [HTTP Layer] [Use Case Layer] [Entity Layer] Express Route Handler --> PlaceOrderInteractor --> Order.create() | Order.calculateTotal() v OrderRepository (interface) ^ | [Persistence Layer] PostgresOrderRepository (implements OrderRepository) ``` Dependencies: - Express Route Handler depends on PlaceOrderInteractor (inward) -- correct - PlaceOrderInteractor depends on Order (inward) -- correct - PlaceOrderInteractor depends on OrderRepository interface (same circle) -- correct - PostgresOrderRepository depends on OrderRepository interface (inward) -- correct - Express Route Handler does NOT appear in any inner circle -- correct - PostgreSQL does NOT appear in any inner circle -- correct The Dependency Rule is satisfied. The business rules (Order, PlaceOrderInteractor) know nothing about Express or PostgreSQL. You could swap both without changing a single line of business logic. -
entities-use-cases.md 14.4 KB
# Entities and Use Cases Entities and Use Cases form the two innermost circles of Clean Architecture. Entities contain Enterprise Business Rules -- the most general and highest-level rules. Use Cases contain Application Business Rules -- the automation rules specific to a particular application. Together, they represent the core value of the system, the code that is most worth protecting from external change. This reference covers entity design, use case structure, the interactor pattern, input/output boundaries, request/response models, and strategies for keeping use cases focused. ## Table of Contents 1. [Enterprise Business Rules (Entities)](#enterprise-business-rules-entities) 2. [Application Business Rules (Use Cases)](#application-business-rules-use-cases) 3. [Request and Response Models](#request-and-response-models) 4. [Keeping Use Cases Focused](#keeping-use-cases-focused) --- ## Enterprise Business Rules (Entities) ### What Is an Entity? An entity is an object within the system that embodies a small set of critical business rules operating on critical business data. The entity object either contains the critical business data or has easy access to it. The interface of the entity consists of the functions that implement the critical business rules. **Critical distinction:** An entity is not a database row. It is not an ORM model. It is not a struct that merely holds data. An entity encapsulates business rules -- logic that would exist even if there were no computer system at all. ### Characteristics of Well-Designed Entities **1. Framework-independent:** Entities do not inherit from database base classes, do not carry ORM annotations, and do not import framework packages. ```python # WRONG: Entity coupled to ORM class Order(db.Model): # Inherits from SQLAlchemy __tablename__ = 'orders' id = db.Column(db.Integer, primary_key=True) total = db.Column(db.Float) # RIGHT: Pure domain entity class Order: def __init__(self, order_id: str, items: list[OrderItem], customer_id: str): self._id = order_id self._items = items self._customer_id = customer_id self._status = OrderStatus.PENDING def calculate_total(self) -> Money: subtotal = sum(item.price * item.quantity for item in self._items) return subtotal + self._calculate_tax(subtotal) def _calculate_tax(self, subtotal: Money) -> Money: # Business rule: tax calculation return subtotal * Decimal("0.08") ``` **2. Business-rule containers:** The methods on an entity enforce business invariants. They are not getters and setters -- they represent meaningful business operations. ```python class BankAccount: def withdraw(self, amount: Money) -> None: if amount > self._balance: raise InsufficientFundsError(self._balance, amount) if self._is_frozen: raise AccountFrozenError(self._account_id) self._balance -= amount self._record_transaction(TransactionType.WITHDRAWAL, amount) ``` The `withdraw` method encapsulates business rules: you cannot withdraw more than the balance, and you cannot withdraw from a frozen account. These rules exist regardless of whether the system is a web app, a mobile app, or a batch process. **3. Stable over time:** Entities change only when business rules change. A decision to migrate from PostgreSQL to DynamoDB should not require any entity modifications. A decision to change the web framework should not affect entities. **4. Testable in complete isolation:** You should be able to instantiate an entity and call its methods in a unit test with zero setup -- no database, no framework, no configuration files. ```python def test_order_calculates_total_with_tax(): items = [OrderItem("widget", Money("10.00"), quantity=3)] order = Order("order-1", items, "customer-1") assert order.calculate_total() == Money("32.40") # 30.00 + 2.40 tax ``` ### Entity Design Patterns | Pattern | When to Use | Example | |---------|-------------|---------| | **Rich domain model** | Complex business rules with many invariants | `Order` with status transitions, validation, calculations | | **Value objects** | Immutable concepts defined by their attributes | `Money(amount, currency)`, `Address(street, city, zip)` | | **Aggregates** | Cluster of entities treated as a unit for data changes | `Order` aggregate contains `OrderItems`; external code accesses items only through `Order` | | **Domain events** | Communicate that something meaningful happened | `Order.place()` produces `OrderPlaced` event | | **Factory methods** | Complex construction that enforces invariants | `Order.create(items, customer)` validates and initializes | ### Common Entity Mistakes | Mistake | Why It's Wrong | Fix | |---------|---------------|-----| | Anemic entities (data-only, no behavior) | Business rules scatter into services; entity is just a DTO | Move business logic into entity methods | | ORM annotations on domain entities | Entity depends on database framework | Separate domain entity from persistence model | | Entity knows about its repository | Entity depends on infrastructure | Pass dependencies into use cases, not entities | | Public setters on everything | No invariant protection; any code can put entity in invalid state | Use methods that enforce business rules; make fields private | ## Application Business Rules (Use Cases) ### What Is a Use Case? A Use Case describes a single, specific application operation. It orchestrates entities and defines the application-specific rules for how data flows to and from those entities. It accepts input through a defined port, manipulates entities, and produces output through another defined port. **Critical distinction:** Use Cases are not entities. An entity encapsulates a business rule that would exist without software. A Use Case automates a specific application workflow that only makes sense within the context of the software system. ### The Interactor Pattern The Interactor is the concrete class that implements a Use Case. The pattern has three parts: 1. **Input Port (Input Boundary):** An interface that defines what the Use Case accepts. The Controller calls this interface. 2. **Interactor:** The concrete class that implements the Input Port and contains the application logic. 3. **Output Port (Output Boundary):** An interface that defines what the Use Case produces. The Presenter implements this interface. ```python # Input Port -- defined in the Use Case circle class PlaceOrderInput(ABC): @abstractmethod def execute(self, request: PlaceOrderRequest) -> None: pass # Output Port -- defined in the Use Case circle class PlaceOrderOutput(ABC): @abstractmethod def present_success(self, response: OrderResponse) -> None: pass @abstractmethod def present_validation_error(self, errors: list[str]) -> None: pass @abstractmethod def present_failure(self, message: str) -> None: pass # Interactor -- implements Input Port, uses Output Port class PlaceOrderInteractor(PlaceOrderInput): def __init__(self, order_repo: OrderRepository, presenter: PlaceOrderOutput): self._order_repo = order_repo self._presenter = presenter def execute(self, request: PlaceOrderRequest) -> None: errors = self._validate(request) if errors: self._presenter.present_validation_error(errors) return order = Order.create( items=[OrderItem(i.product_id, i.quantity, i.price) for i in request.items], customer_id=request.customer_id, ) self._order_repo.save(order) response = OrderResponse( order_id=order.id, total=order.calculate_total(), status=order.status.value, ) self._presenter.present_success(response) ``` ### Input/Output Boundaries The boundaries are the interfaces that separate the Use Case circle from the circles on either side. They are defined in the Use Case circle and implemented by the outer circles. **Why boundaries matter:** - The Controller depends on the Input Port (inward dependency -- correct) - The Presenter depends on the Output Port (inward dependency -- correct) - The Interactor depends on neither the Controller nor the Presenter (isolation preserved) ### Alternative: Return-Based Use Cases Not every use case needs the full Output Port pattern. A simpler approach returns a result directly: ```python class PlaceOrderInteractor: def __init__(self, order_repo: OrderRepository): self._order_repo = order_repo def execute(self, request: PlaceOrderRequest) -> Result[OrderResponse, OrderError]: errors = self._validate(request) if errors: return Failure(ValidationError(errors)) order = Order.create(...) self._order_repo.save(order) return Success(OrderResponse(order.id, order.calculate_total())) ``` This is simpler and often sufficient. Use the full Output Port pattern when the presentation logic is complex or when you need to support multiple presentation formats from the same use case. ## Request and Response Models ### Request Models Request models are simple data structures that carry input data across the boundary. They are defined in the Use Case circle. **Rules for request models:** - No framework types (no `HttpRequest`, no `Form`, no `JsonNode`) - No entity types (the controller maps external data to the request model; the interactor maps the request model to entity calls) - Contain only primitives, strings, and simple nested structures - May contain validation hints but not validation logic dependent on external state ```python @dataclass(frozen=True) class PlaceOrderRequest: customer_id: str items: list[OrderItemRequest] shipping_address: AddressRequest coupon_code: str | None = None @dataclass(frozen=True) class OrderItemRequest: product_id: str quantity: int unit_price: str # String to avoid floating-point; Use Case converts to Money ``` ### Response Models Response models carry output data across the boundary. They are defined in the Use Case circle. **Rules for response models:** - No entity types -- the Use Case extracts the relevant data from entities and populates the response - No framework types -- the Presenter (outer circle) converts the response into whatever format the delivery mechanism needs - Contain only the data the outer circle needs to fulfill its role ```python @dataclass(frozen=True) class OrderResponse: order_id: str total: str # Formatted money value status: str estimated_delivery: str | None items: list[OrderItemResponse] @dataclass(frozen=True) class OrderItemResponse: product_name: str quantity: int line_total: str ``` ### The Mapping Chain Data transforms at each boundary: ``` HTTP Request (JSON) --> Controller maps to --> PlaceOrderRequest (DTO) --> Interactor maps to --> Entity method calls --> Entity produces result --> Interactor maps to --> OrderResponse (DTO) --> Presenter maps to --> ViewModel or JSON --> HTTP Response ``` Each transformation is a boundary crossing. Each boundary is an opportunity to decouple. ## Keeping Use Cases Focused ### One Use Case, One Operation Each Use Case should represent a single application operation. If you find a Use Case doing multiple things, split it. **Signs of an unfocused Use Case:** - The class name contains "And" (e.g., `CreateAndNotifyOrder`) - The execute method has conditional branches for fundamentally different operations - The class has more than 3-4 dependencies - The test file has tests for unrelated scenarios ### Use Case Granularity Guidelines | Granularity | Use Case Example | Notes | |-------------|-----------------|-------| | **Too coarse** | `ManageOrders` | Does everything -- create, update, cancel, refund | | **Right level** | `PlaceOrder`, `CancelOrder`, `RefundOrder` | Each is a single operation with clear input and output | | **Too fine** | `ValidateOrderItems`, `CalculateOrderTotal` | These are steps within a use case, not standalone operations | ### Composing Use Cases Sometimes one application operation involves multiple steps that could be their own use cases. Two approaches: **1. Use Case calls Use Case (simple composition):** ```python class PlaceOrderAndSendConfirmation: def __init__(self, place_order: PlaceOrderInput, send_confirmation: SendConfirmationInput): self._place_order = place_order self._send_confirmation = send_confirmation def execute(self, request: PlaceOrderRequest) -> None: order_result = self._place_order.execute(request) if order_result.is_success: self._send_confirmation.execute( SendConfirmationRequest(order_result.order_id) ) ``` **2. Domain events (loose coupling):** The Use Case emits a domain event; another Use Case subscribes to it. This is better when the steps are truly independent and could happen asynchronously. ### Use Case Dependencies A Use Case should depend on: - **Entity types** (to call business rules) - **Repository interfaces** (to load and persist entities) - **Output port interfaces** (to present results) - **Domain service interfaces** (for cross-entity business operations) A Use Case should NOT depend on: - **Framework types** (HTTP, ORM, message queue) - **Concrete infrastructure classes** (database client, email service) - **Other use case concrete classes** (use input port interfaces instead) - **Configuration or environment variables** (inject configuration as constructor parameters) ### Testing Use Cases Use Cases should be the most thoroughly tested part of the system because they contain the application's automation rules. ```python def test_place_order_calculates_total_and_saves(): # Arrange mock_repo = MockOrderRepository() mock_presenter = MockPlaceOrderPresenter() interactor = PlaceOrderInteractor(mock_repo, mock_presenter) request = PlaceOrderRequest( customer_id="cust-1", items=[OrderItemRequest("prod-1", quantity=2, unit_price="25.00")], shipping_address=AddressRequest("123 Main", "Springfield", "62704"), ) # Act interactor.execute(request) # Assert assert mock_repo.saved_order is not None assert mock_repo.saved_order.calculate_total() == Money("54.00") # 50 + 4 tax assert mock_presenter.success_response.order_id == mock_repo.saved_order.id ``` No database. No web server. No framework. Just the use case logic running in a plain unit test. This is the payoff of the Dependency Rule. -
solid-principles.md 16.5 KB
# SOLID Principles The SOLID principles are five design principles for managing dependencies at the class and module level. They were assembled and named by Robert C. Martin in the early 2000s, drawing on decades of software engineering wisdom. In Clean Architecture, SOLID principles serve as the mid-level building blocks that make the Dependency Rule possible. Without SOLID, the concentric circles would leak and the boundaries would crumble. This reference covers each principle with definitions, code examples, common violations, and practical application guidance. ## Table of Contents 1. [SRP: The Single Responsibility Principle](#srp-the-single-responsibility-principle) 2. [OCP: The Open-Closed Principle](#ocp-the-open-closed-principle) 3. [LSP: The Liskov Substitution Principle](#lsp-the-liskov-substitution-principle) 4. [ISP: The Interface Segregation Principle](#isp-the-interface-segregation-principle) 5. [DIP: The Dependency Inversion Principle](#dip-the-dependency-inversion-principle) --- ## SRP: The Single Responsibility Principle **"A module should have one, and only one, reason to change."** More precisely: a module should be responsible to one, and only one, actor (a group of users or stakeholders who want the system to change in the same way). ### Understanding SRP SRP is commonly misunderstood as "a function should do one thing." That's a good principle for functions, but SRP operates at a higher level. SRP says that the module (class) should serve one actor -- one group of people who would request changes. ### Classic Violation ```python class Employee: def calculate_pay(self) -> Money: # Serves the CFO / accounting department regular_hours = self._get_regular_hours() overtime = self._get_overtime_hours() return regular_hours * self.hourly_rate + overtime * self.hourly_rate * 1.5 def report_hours(self) -> HoursReport: # Serves the COO / operations department return HoursReport( regular=self._get_regular_hours(), overtime=self._get_overtime_hours(), ) def save(self) -> None: # Serves the CTO / database administrators db.execute("INSERT INTO employees ...", self._to_dict()) def _get_regular_hours(self) -> float: # Shared by calculate_pay and report_hours -- dangerous coupling return min(self.hours_worked, 40) def _get_overtime_hours(self) -> float: return max(self.hours_worked - 40, 0) ``` **The problem:** Three actors (CFO, COO, CTO) all have reasons to change this class. When the CFO wants to change how overtime is calculated, the shared `_get_regular_hours` method might be modified in a way that breaks the COO's reports. ### SRP-Compliant Design ```python class PayCalculator: """Serves the CFO / accounting""" def calculate_pay(self, employee_data: EmployeeData) -> Money: regular = min(employee_data.hours_worked, 40) overtime = max(employee_data.hours_worked - 40, 0) return regular * employee_data.rate + overtime * employee_data.rate * 1.5 class HoursReporter: """Serves the COO / operations""" def report_hours(self, employee_data: EmployeeData) -> HoursReport: return HoursReport( regular=min(employee_data.hours_worked, 40), overtime=max(employee_data.hours_worked - 40, 0), ) class EmployeeRepository: """Serves the CTO / database administration""" def save(self, employee_data: EmployeeData) -> None: self._db.execute("INSERT INTO employees ...", employee_data.to_dict()) ``` Each class now serves one actor. Changes requested by the CFO only affect `PayCalculator`. The COO's changes only affect `HoursReporter`. They can evolve independently. ### SRP Indicators | Indicator | Likely Violation | |-----------|-----------------| | Class has methods serving different departments/teams | Multiple actors | | "And" in the class name (`OrderValidatorAndNotifier`) | Multiple responsibilities | | Class changes frequently for unrelated reasons | Multiple change drivers | | Merge conflicts from unrelated feature branches | Multiple actors modifying same class | | Unit tests require many unrelated mocks | Class does too many things | ## OCP: The Open-Closed Principle **"A software artifact should be open for extension but closed for modification."** You should be able to extend the behavior of a system without modifying existing code. New features are added by writing new code, not by changing old code. ### The Strategy Pattern Approach ```python # Closed for modification -- this code doesn't change when new shipping methods are added class OrderService: def __init__(self, shipping_strategy: ShippingStrategy): self._shipping = shipping_strategy def calculate_total(self, order: Order) -> Money: subtotal = order.subtotal() shipping = self._shipping.calculate(order) return subtotal + shipping # Open for extension -- add new shipping methods without touching OrderService class ShippingStrategy(ABC): @abstractmethod def calculate(self, order: Order) -> Money: pass class StandardShipping(ShippingStrategy): def calculate(self, order: Order) -> Money: return Money("5.99") class ExpressShipping(ShippingStrategy): def calculate(self, order: Order) -> Money: return Money("14.99") # New shipping method -- no existing code modified class FreeShippingOver50(ShippingStrategy): def calculate(self, order: Order) -> Money: return Money("0.00") if order.subtotal() >= Money("50.00") else Money("5.99") ``` ### Common OCP Violations ```python # VIOLATION: Adding a new payment method requires modifying this function def process_payment(method: str, amount: Money) -> PaymentResult: if method == "credit_card": return charge_credit_card(amount) elif method == "paypal": return charge_paypal(amount) elif method == "apple_pay": # New method = new elif = modification return charge_apple_pay(amount) ``` **Fix with OCP:** ```python class PaymentProcessor(ABC): @abstractmethod def process(self, amount: Money) -> PaymentResult: pass class CreditCardProcessor(PaymentProcessor): def process(self, amount: Money) -> PaymentResult: return self._gateway.charge(amount) # Adding Apple Pay = new class, no modification to existing code class ApplePayProcessor(PaymentProcessor): def process(self, amount: Money) -> PaymentResult: return self._apple_client.charge(amount) ``` ### OCP in Clean Architecture OCP is foundational to the concentric circles model. The inner circles (entities, use cases) are closed for modification. The outer circles (adapters, frameworks) are open for extension. You extend the system by adding new adapters, new controllers, new gateways -- not by modifying business rules. ## LSP: The Liskov Substitution Principle **"Subtypes must be substitutable for their base types."** If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program. ### The Classic Violation: Square/Rectangle ```python class Rectangle: def __init__(self, width: float, height: float): self._width = width self._height = height def set_width(self, w: float) -> None: self._width = w def set_height(self, h: float) -> None: self._height = h def area(self) -> float: return self._width * self._height class Square(Rectangle): def set_width(self, w: float) -> None: self._width = w self._height = w # Must keep square invariant def set_height(self, h: float) -> None: self._width = h # Must keep square invariant self._height = h ``` **The problem:** Code that works correctly with `Rectangle` breaks with `Square`: ```python def test_area(rect: Rectangle): rect.set_width(5) rect.set_height(4) assert rect.area() == 20 # Fails for Square! Area is 16 because set_height changed width ``` `Square` is NOT substitutable for `Rectangle`. LSP is violated. ### LSP in Practice | Violation Pattern | Why It Breaks | Fix | |-------------------|--------------|-----| | Subclass throws unexpected exceptions | Callers don't handle exceptions they didn't expect from the base type | Subclass should honor the base type's exception contract | | Subclass ignores methods (no-op override) | Callers rely on the method doing something | The class hierarchy is wrong; use composition or a different abstraction | | Subclass strengthens preconditions | Callers that work with base type fail with subtype | Subtypes may weaken preconditions, never strengthen them | | Subclass weakens postconditions | Callers expect guarantees the subtype doesn't provide | Subtypes may strengthen postconditions, never weaken them | ### LSP and Interfaces in Clean Architecture LSP applies to interfaces as well as inheritance hierarchies. When a Use Case depends on `OrderRepository`, every implementation (`PostgresOrderRepository`, `MongoOrderRepository`, `InMemoryOrderRepository`) must behave consistently: - `save()` must persist the entity (or fail with a defined exception) - `find_by_id()` must return the entity if it exists or `None` if not - No implementation should silently drop data, return stale data, or throw exceptions not defined in the interface contract ## ISP: The Interface Segregation Principle **"No client should be forced to depend on methods it does not use."** Fat interfaces create unnecessary coupling. When a client depends on an interface with methods it doesn't use, it becomes vulnerable to changes in those unused methods. ### Classic Violation ```python class MultiFunctionDevice(ABC): @abstractmethod def print_document(self, doc: Document) -> None: pass @abstractmethod def scan_document(self) -> Image: pass @abstractmethod def fax_document(self, doc: Document, number: str) -> None: pass @abstractmethod def staple_pages(self, pages: list[Page]) -> None: pass # A simple printer must implement fax and staple -- methods it can't fulfill class SimplePrinter(MultiFunctionDevice): def print_document(self, doc: Document) -> None: # Actually prints ... def scan_document(self) -> Image: raise NotSupportedError() # ISP violation! def fax_document(self, doc: Document, number: str) -> None: raise NotSupportedError() # ISP violation! def staple_pages(self, pages: list[Page]) -> None: raise NotSupportedError() # ISP violation! ``` ### ISP-Compliant Design ```python class Printer(ABC): @abstractmethod def print_document(self, doc: Document) -> None: pass class Scanner(ABC): @abstractmethod def scan_document(self) -> Image: pass class FaxMachine(ABC): @abstractmethod def fax_document(self, doc: Document, number: str) -> None: pass # Simple printer only implements what it can do class SimplePrinter(Printer): def print_document(self, doc: Document) -> None: ... # Multi-function device implements all relevant interfaces class OfficePrinter(Printer, Scanner, FaxMachine): def print_document(self, doc: Document) -> None: ... def scan_document(self) -> Image: ... def fax_document(self, doc: Document, number: str) -> None: ... ``` ### ISP in Clean Architecture ISP directly supports the Dependency Rule. Use Cases define narrow, focused input and output port interfaces. Each adapter implements only the interfaces it needs: ```python # Focused interfaces (ISP-compliant) class OrderReader(ABC): @abstractmethod def find_by_id(self, order_id: str) -> Order | None: pass class OrderWriter(ABC): @abstractmethod def save(self, order: Order) -> None: pass class OrderSearcher(ABC): @abstractmethod def search(self, criteria: SearchCriteria) -> list[Order]: pass # Use Case that only reads doesn't depend on write methods class GetOrderDetailsInteractor: def __init__(self, reader: OrderReader): # Only depends on reading self._reader = reader ``` ## DIP: The Dependency Inversion Principle **"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions."** DIP is the mechanism that makes the Dependency Rule work. It inverts the natural direction of source code dependencies so that the volatile, concrete, outer-circle code depends on the stable, abstract, inner-circle code. ### Without DIP (Natural Dependencies) ``` OrderService --> PostgresDatabase (high-level) (low-level) ``` The high-level policy (OrderService) depends on the low-level detail (PostgresDatabase). Changing the database means changing the service. ### With DIP (Inverted Dependencies) ``` OrderService --> OrderRepository (interface) ^ | PostgresOrderRepository ``` Both the high-level service and the low-level database adapter depend on the abstraction (OrderRepository). The abstraction is defined by the high-level module, not by the low-level module. ### DIP Implementation Pattern ```python # HIGH-LEVEL MODULE defines the abstraction class OrderRepository(ABC): """Defined in the Use Case circle. The high-level policy dictates what it needs.""" @abstractmethod def save(self, order: Order) -> None: pass @abstractmethod def find_by_id(self, order_id: str) -> Order | None: pass # HIGH-LEVEL MODULE uses the abstraction class PlaceOrderInteractor: def __init__(self, repo: OrderRepository): # Depends on abstraction self._repo = repo def execute(self, request: PlaceOrderRequest) -> None: order = Order.create(request.items, request.customer_id) self._repo.save(order) # Calls abstraction # LOW-LEVEL MODULE implements the abstraction class PostgresOrderRepository(OrderRepository): # Depends on abstraction def __init__(self, pool): self._pool = pool def save(self, order: Order) -> None: # SQL details here -- low-level ... # COMPOSITION ROOT wires them together def main(): pool = create_pool(DATABASE_URL) repo = PostgresOrderRepository(pool) interactor = PlaceOrderInteractor(repo) # Inject concrete into abstract slot ``` ### DIP: Who Owns the Interface? This is the critical insight: **the interface belongs to the high-level module, not the low-level module.** | Ownership | Meaning | Result | |-----------|---------|--------| | Interface owned by high-level module | The Use Case defines what it needs | Low-level module adapts to high-level needs | | Interface owned by low-level module | The database defines its capabilities | High-level module must adapt to database -- dependency NOT inverted | When the Use Case defines `OrderRepository`, it specifies methods like `save(order)` and `find_by_id(id)` -- business-oriented operations. The database adapter must conform to this business-oriented interface. When the database adapter defines the interface, it specifies methods like `execute_query(sql)` and `fetch_rows(table)` -- technology-oriented operations. The Use Case must conform to the database's way of thinking. This is the natural dependency direction, NOT inverted. ### Common DIP Violations | Violation | Example | Fix | |-----------|---------|-----| | Importing concrete classes in high-level modules | `from stripe import StripeClient` in Use Case | Define `PaymentGateway` interface in Use Case; implement with Stripe in adapter | | Using static/global factory methods | `Database.get_instance()` in Use Case | Inject repository through constructor | | Depending on framework types in domain | `@Autowired` on domain class | Use plain constructor injection; wire in Main | | Low-level module defines the interface | `IStripeGateway` lives in the Stripe adapter package | Move interface to Use Case package; rename to `PaymentGateway` | | New operator in high-level code | `repo = PostgresRepository()` inside Use Case | Inject through constructor; instantiate in Main | ### DIP and Clean Architecture DIP is the engine of Clean Architecture. Every boundary in the concentric circles model is maintained through dependency inversion: - **Use Case to Database:** `OrderRepository` interface (defined by Use Case) inverts the dependency so the database adapter depends inward - **Use Case to Web:** `PlaceOrderOutput` interface (defined by Use Case) inverts the dependency so the presenter depends inward - **Use Case to External Service:** `EmailService` interface (defined by Use Case) inverts the dependency so the email adapter depends inward Without DIP, inner circles would depend on outer circles, the Dependency Rule would be violated, and the architecture would collapse into a ball of mud.
-
-
SKILL.md 16 KB
--- name: clean-architecture description: 'Structure software around the Dependency Rule: source code dependencies point inward from frameworks to use cases to entities. Use when the user mentions "architecture layers", "dependency rule", "ports and adapters (hexagonal)", "onion architecture", "screaming architecture", "where should business logic go", "decouple from the database", "swap the framework without a rewrite", or "keep business rules independent". Also trigger when deciding which layer code belongs in, isolating core logic from infrastructure, defining module boundaries, or debating whether the framework should call your code or the reverse. Covers component principles, boundaries, and SOLID. For code-level quality, see clean-code. For domain modeling, see domain-driven-design.' license: MIT metadata: author: wondelai version: "1.4.0" --- # Clean Architecture Framework A disciplined approach to structuring software so that business rules remain independent of frameworks, databases, and delivery mechanisms. Apply these principles when designing system architecture, reviewing module boundaries, or advising on dependency management. ## Core Principle **Source code dependencies must point inward — toward higher-level policies.** Nothing in an inner circle can know anything about an outer circle. This single rule produces systems that are testable and independent of frameworks, UI, database, and any external agency. Business rules are what matter; databases, web frameworks, and delivery mechanisms are details — when details depend on policies, you can defer decisions, swap implementations, and test business logic in isolation. ## Scoring **Goal: 10/10.** Score one point for each of the seven Quick Diagnostic rows the architecture satisfies (0-7), then map to a 0-10 band: 6-7 satisfied = **9-10** (Dependency Rule holds, business logic is framework- and DB-independent); 4-5 = **6-8** (core is testable but some details leak inward); 2-3 = **3-5** (framework or persistence dictates structure); 0-1 = **0-2** (no boundaries — business rules live in controllers and ORM models). Report the score, the failed diagnostic rows, and the specific inversion needed to fix each. ### 1. Dependency Rule and Concentric Circles **Core concept:** Organize the architecture as concentric circles — Entities (enterprise business rules) innermost, then Use Cases (application business rules), then Interface Adapters, with Frameworks and Drivers outermost. Source code dependencies always point inward. **Why it works:** When high-level policies don't depend on low-level details, you can swap the database, web framework, or API style without touching business logic — the system becomes resilient to the most volatile parts of the stack. **Key insights:** - Inner circles cannot mention outer circle names — no classes, functions, variables, or data formats from outside - Data crossing a boundary must be in the form most convenient for the inner circle, never dictated by the outer - Dependency Inversion (interfaces defined inward, implemented outward) is the mechanism that enforces the rule - The number of circles is not fixed — four is typical; the rule stays the same - Frameworks are details, not architecture — they belong in the outermost circle **Code applications:** | Context | Pattern | Example | |---------|---------|---------| | **Layer direction** | Inner circles define interfaces; outer implement | `UserRepository` interface in Use Cases; `PostgresUserRepository` in Adapters | | **Data crossing** | DTOs cross boundaries, not ORM entities | Use Case returns `UserResponse` DTO, not an ActiveRecord model | | **Dependency direction** | Import arrows always point inward | Controller imports Use Case; Use Case never imports Controller | See [references/dependency-rule.md](references/dependency-rule.md) when an inner-circle import points outward and you need the four-circle code walkthrough, the data-crossing rules, and the four-step dependency-inversion procedure to fix it. ### 2. Entities and Use Cases **Core concept:** Entities encapsulate enterprise-wide business rules — rules that would exist even without software. Use Cases contain application-specific rules that orchestrate the flow of data to and from Entities. **Why it works:** Separating what the business does (Entities) from how the application orchestrates it (Use Cases) lets you reuse Entities across applications and change application behavior without altering core business rules. **Key insights:** - Entities are not database rows — they are objects or pure functions encapsulating critical business rules - Use Cases accept Request Models and return Response Models — never framework objects - Each Use Case is a single application operation (`CreateOrder`, `ApproveExpense`) - The Interactor pattern: a Use Case class implements an input boundary interface and calls an output boundary interface - Changes to a Use Case should never affect an Entity; Entity changes may ripple to Use Cases **Code applications:** | Context | Pattern | Example | |---------|---------|---------| | **Entity design** | Critical business rules, zero framework dependencies | `Order.calculateTotal()` applies tax rules; knows nothing about HTTP | | **Request/Response** | Simple data structures cross the boundary | `CreateOrderRequest { items, customerId }` — no ORM models | | **Single responsibility** | One Use Case per operation | `PlaceOrder`, `CancelOrder`, `RefundOrder` as separate classes | | **Interactor** | Implements Input Port, calls Output Port | `PlaceOrderInteractor implements PlaceOrderInput` | See [references/entities-use-cases.md](references/entities-use-cases.md) when designing an Interactor or deciding what belongs in an Entity versus a Use Case — full Enterprise vs. Application Business Rules treatment with request/response model examples. ### 3. Interface Adapters and Frameworks **Core concept:** Interface Adapters convert data between the form convenient for Use Cases/Entities and the form required by external agencies. Frameworks and Drivers are the outermost layer — glue code to the outside world. **Why it works:** When the web framework, ORM, or message queue is confined to the outer circles, replacing any of them is a localized change. The database is a detail; the web is a detail; details should be plugins to your business rules, not the skeleton of the application. **Key insights:** - Controllers translate HTTP into Use Case input; Presenters translate Use Case output into view models - Gateways implement repository interfaces defined by Use Cases — the inner circle defines the contract, the outer fulfills it - Business rules never know whether data lives in SQL, NoSQL, or flat files, or that delivery is HTTP - Treat frameworks with suspicion — they want you to couple to them; keep them at arm's length **Code applications:** | Context | Pattern | Example | |---------|---------|---------| | **Controller** | Delivery mechanism → Use Case input | `OrderController.create(req)` builds `CreateOrderRequest`, calls Interactor | | **Presenter** | Use Case output → view model | `OrderPresenter.present(response)` formats for JSON/HTML | | **Gateway** | Repository interface implemented per DB | `SqlOrderRepository implements OrderRepository` | | **Framework boundary** | Framework calls inward, never the reverse | Express route handler calls Controller; Controller never imports Express | See [references/adapters-frameworks.md](references/adapters-frameworks.md) when wiring controllers, presenters, or gateways, or arguing that the database/web is a detail — covers plugin architecture and how to confine a framework to the edges. ### 4. Component Principles **Core concept:** Components are the units of deployment. Three cohesion principles govern what goes inside a component; three coupling principles govern relationships between components. **Why it works:** Poorly composed components create ripple effects where one change forces redeployment of unrelated code; the principles keep changes localized and releases independent. **Key insights:** - REP (Reuse/Release Equivalence): classes in a component must be versionable and releasable as a unit - CCP (Common Closure): classes that change for the same reason at the same time belong together — SRP for components - CRP (Common Reuse): don't force users to depend on classes they don't use - ADP (Acyclic Dependencies): the component graph must have no cycles — break them with DIP or a new component - SDP (Stable Dependencies): depend in the direction of stability - SAP (Stable Abstractions): stable components should be abstract; unstable ones concrete **Code applications:** | Context | Pattern | Example | |---------|---------|---------| | **Component grouping** | Group classes that change together (CCP) | All order-related Use Cases in one component | | **Breaking cycles** | Apply DIP to invert a dependency edge | Extract an interface into a new component to break the cycle | | **Stability metrics** | Instability I = Ce / (Ca + Ce) | Many incoming, no outgoing deps → I near 0 (stable) | See [references/component-principles.md](references/component-principles.md) when grouping classes into deployable components or breaking a dependency cycle — each of REP, CCP, CRP, ADP, SDP, SAP worked through with the instability metric. ### 5. SOLID Principles **Core concept:** Five class-and-module-level principles — Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion — the mid-level building blocks that make the Dependency Rule possible. **Why it works:** Each principle addresses a specific way dependencies go wrong, preventing the rigidity, fragility, and immobility that turn codebases into legacy nightmares. **Key insights:** - SRP: a module has one reason to change — it serves one actor (not "does one thing") - OCP: extend behavior by adding new code, not modifying existing code — strategy and plugin patterns - LSP: subtypes must be usable through the base interface without the client knowing — violated by unexpected exceptions or ignored methods - ISP: clients should not depend on methods they don't use — fat interfaces create needless coupling - DIP: high-level modules and low-level modules both depend on abstractions defined by the high-level module **Code applications:** | Context | Pattern | Example | |---------|---------|---------| | **SRP violation** | Class serves multiple actors | `Employee` handles pay (CFO), reporting (COO), persistence (CTO) | | **OCP via strategy** | New behavior through new classes | Add `ExpressShipping` implementing `ShippingStrategy`; `Order` untouched | | **LSP violation** | Subtype changes expected behavior | `Square extends Rectangle` breaks the `setWidth()`/`setHeight()` contract | | **ISP application** | Split fat interfaces into role interfaces | `Printer`, `Scanner`, `Fax` instead of one `MultiFunctionDevice` | | **DIP wiring** | High-level defines interface; low-level implements | `OrderService` depends on `PaymentGateway`, not `StripeClient` | See [references/solid-principles.md](references/solid-principles.md) when applying SRP/OCP/LSP/ISP/DIP to a specific class or diagnosing a violation — each principle worked through with code examples and the smell it prevents. ### 6. Boundaries and Boundary Anatomy **Core concept:** A boundary is a line between things that matter and things that are details, implemented through polymorphism: dependencies cross pointing inward while control flow may cross either way. **Why it works:** Every boundary buys the option to defer a decision or swap an implementation; strategic boundary placement determines whether a system is a joy or a pain to maintain over years. **Key insights:** - Full boundaries use reciprocal interfaces on both sides; partial boundaries use a simpler strategy or facade - Humble Object pattern: split boundary code into a hard-to-test part (close to the boundary) and an easy-to-test part (the logic) - Services are not automatically architectural boundaries — a microservice with a fat shared data model is a monolith with network calls - Tests are the most isolated component: they depend inward, nothing depends on them - Premature boundaries are expensive, but so are missing ones — draw them at points of likely volatility **Code applications:** | Context | Pattern | Example | |---------|---------|---------| | **Full vs. partial boundary** | Reciprocal ports, or a lone strategy | Use Case defines `PlaceOrderInput`/`PlaceOrderOutput`; simpler cases take a `ShippingStrategy` | | **Humble Object** | Separate testable logic from infrastructure | `PresenterLogic` (testable) produces `ViewModel`; `View` (humble) renders it | | **Main as plugin** | Composition root assembles the system | `main()` wires all concrete implementations and starts the app | See [references/boundaries.md](references/boundaries.md) when deciding where to draw a boundary, choosing full vs. partial, or applying the Humble Object pattern — also covers services as boundaries, test boundaries, and Main as the ultimate plugin. ## Common Mistakes | Mistake | Why It Fails | Fix | |---------|-------------|-----| | **ORM leaking into business logic** | Entities couple to the schema; DB changes rewrite business rules | Separate domain entities from persistence models; map at the adapter layer | | **Business rules in controllers** | Untestable without HTTP; duplicated across endpoints | Move logic into Use Case Interactors; controllers only translate and delegate | | **Framework-first architecture** | Framework dictates structure; swapping means a rewrite | Treat the framework as a plugin; structure code by business capability | | **Circular component dependencies** | Changes ripple unpredictably; no independent releases | Apply DIP or extract a shared abstraction component | | **One giant Use Case per feature** | Bloated thousand-line orchestrators | Split into focused single-operation Use Cases | | **Skipping boundaries "because it's simple"** | Coupling accumulates silently until the cost is enormous | Draw boundaries proactively at points of likely volatility | | **Microservices as automatic good architecture** | A distributed monolith is worse than a clean monolith | Apply the Dependency Rule within and across services; services are deployment boundaries, not architectural ones | ## Quick Diagnostic | Question | If No | Action | |----------|-------|--------| | Can you test business rules without DB, web server, or framework? | Rules coupled to infrastructure | Extract entities and use cases behind interfaces; mock outer layers | | Do all source dependencies point inward? | Dependency Rule violated | Introduce boundary interfaces; invert the offending dependency | | Can you swap the database without touching business logic? | Persistence leaking inward | Repository pattern; isolate persistence in adapters | | Are Use Cases independent of delivery mechanism? | Use Cases know HTTP/CLI/queues | Use plain DTOs in Use Case signatures | | Is the framework confined to the outermost circle? | Framework is your architecture | Wrap framework calls behind interfaces; push to the edges | | Is the component graph cycle-free? | Circular dependencies exist | Apply ADP: DIP or new components to break every cycle | | Does Main (composition root) wire all dependencies? | Concrete classes instantiated in inner circles | Move construction to Main; use DI or factories | ## Further Reading Based on Robert C. Martin's definitive guide to software architecture: - [*"Clean Architecture: A Craftsman's Guide to Software Structure and Design"*](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164?tag=wondelai00-20) by Robert C. Martin ## About the Author **Robert C. Martin ("Uncle Bob")** is a software engineer programming since 1970, a founding signatory of the Agile Manifesto, and the author of *Clean Code*, *The Clean Coder*, *Clean Architecture*, and *Clean Agile*. His SOLID principles are foundational vocabulary in object-oriented design, and his work argues that architecture is about managing dependencies and keeping business rules independent of infrastructure details.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.