Interface adapters and frameworks skill

Interface Adapters and Frameworks & Drivers form the two outermost circles of Clean Architecture.

by wondelai·MIT license·★ 2,235 Stars on the repo·GitHub ↗

Use now

Files of Interface adapters and frameworks

wondelai/main1 file
adapters-frameworks.md
Show the full text359 lines

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
  2. Frameworks as Details
  3. The Database Is a Detail
  4. The Web Is a Detail
  5. Plugin Architecture
  6. Keeping Frameworks at Arm's 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
# 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
# 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.

# 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.

# 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:

# 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.

1# Interface Adapters and Frameworks
2 
3Interface 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.
4 
5This 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.
6 
7 
8## Table of Contents
91. [Interface Adapters](#interface-adapters)
102. [Frameworks as Details](#frameworks-as-details)
113. [The Database Is a Detail](#the-database-is-a-detail)
124. [The Web Is a Detail](#the-web-is-a-detail)
135. [Plugin Architecture](#plugin-architecture)
146. [Keeping Frameworks at Arm's Length](#keeping-frameworks-at-arms-length)
15 
16---
17 
18## Interface Adapters
19 
20### Controllers
21 
22A 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.
23 
24**Responsibilities of a Controller:**
25- Parse and extract data from the delivery mechanism's native format
26- Construct the Use Case's Request Model
27- Call the Use Case's Input Port
28- Handle delivery-mechanism-specific concerns (authentication, rate limiting) BEFORE calling the Use Case
29 
30**What a Controller must NOT do:**
31- Contain business logic
32- Directly access the database
33- Format output for the response (that's the Presenter's job)
34- Know about other controllers
35 
36```python
37# Controller in the Adapters circle
38class OrderController:
39 def __init__(self, place_order: PlaceOrderInput):
40 self._place_order = place_order
41 
42 def create(self, http_request: dict) -> None:
43 # Translate HTTP data to Use Case request
44 request = PlaceOrderRequest(
45 customer_id=http_request["customer_id"],
46 items=[
47 OrderItemRequest(
48 product_id=item["product_id"],
49 quantity=item["quantity"],
50 unit_price=item["unit_price"],
51 )
52 for item in http_request["items"]
53 ],
54 shipping_address=AddressRequest(
55 street=http_request["address"]["street"],
56 city=http_request["address"]["city"],
57 zip_code=http_request["address"]["zip"],
58 ),
59 )
60 # Delegate to the Use Case
61 self._place_order.execute(request)
62```
63 
64The Controller knows about HTTP data format and knows about `PlaceOrderRequest`. It translates between the two. The Use Case never sees HTTP.
65 
66### Presenters
67 
68A 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.
69 
70**The Presenter pattern separates two concerns:**
711. The Use Case decides WHAT data to present
722. The Presenter decides HOW to format it for display
73 
74```python
75# Output Port defined in Use Case circle
76class PlaceOrderOutput(ABC):
77 @abstractmethod
78 def present_success(self, response: OrderResponse) -> None:
79 pass
80 
81 @abstractmethod
82 def present_failure(self, message: str) -> None:
83 pass
84 
85# Presenter in the Adapters circle
86class JsonOrderPresenter(PlaceOrderOutput):
87 def __init__(self):
88 self.view_model: dict = {}
89 self.status_code: int = 200
90 
91 def present_success(self, response: OrderResponse) -> None:
92 self.status_code = 201
93 self.view_model = {
94 "data": {
95 "id": response.order_id,
96 "total": f"${response.total}",
97 "status": response.status.capitalize(),
98 "estimated_delivery": response.estimated_delivery,
99 }
100 }
101 
102 def present_failure(self, message: str) -> None:
103 self.status_code = 400
104 self.view_model = {"error": {"message": message}}
105```
106 
107The Presenter knows about JSON structure, status codes, and string formatting. The Use Case knows nothing about any of this.
108 
109### Gateways
110 
111A 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.
112 
113```python
114# Interface defined in Use Case circle
115class OrderRepository(ABC):
116 @abstractmethod
117 def save(self, order: Order) -> None:
118 pass
119 
120 @abstractmethod
121 def find_by_id(self, order_id: str) -> Order | None:
122 pass
123 
124# Gateway in the Adapters circle
125class PostgresOrderRepository(OrderRepository):
126 def __init__(self, connection_pool):
127 self._pool = connection_pool
128 
129 def save(self, order: Order) -> None:
130 with self._pool.connection() as conn:
131 conn.execute(
132 "INSERT INTO orders (id, customer_id, total, status) VALUES (%s, %s, %s, %s)",
133 (order.id, order.customer_id, str(order.calculate_total()), order.status.value),
134 )
135 for item in order.items:
136 conn.execute(
137 "INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (%s, %s, %s, %s)",
138 (order.id, item.product_id, item.quantity, str(item.price)),
139 )
140 
141 def find_by_id(self, order_id: str) -> Order | None:
142 with self._pool.connection() as conn:
143 row = conn.execute("SELECT * FROM orders WHERE id = %s", (order_id,)).fetchone()
144 if row is None:
145 return None
146 items = conn.execute("SELECT * FROM order_items WHERE order_id = %s", (order_id,)).fetchall()
147 return self._to_domain(row, items)
148 
149 def _to_domain(self, row, item_rows) -> Order:
150 # Map database rows back to domain entity
151 items = [OrderItem(r["product_id"], r["quantity"], Money(r["price"])) for r in item_rows]
152 return Order(order_id=row["id"], items=items, customer_id=row["customer_id"])
153```
154 
155Notice 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.
156 
157### Adapter Types Summary
158 
159| Adapter | Translates From | Translates To | Direction |
160|---------|----------------|---------------|-----------|
161| **Controller** | External input (HTTP, CLI, event) | Use Case Request Model | Inward |
162| **Presenter** | Use Case Response Model | View Model (JSON, HTML, CLI output) | Outward |
163| **Gateway** | Repository/Service Interface | Concrete technology (SQL, API, file) | Outward |
164| **Mapper** | Domain Entity | Persistence Model (ORM, document) | Both directions |
165 
166## Frameworks as Details
167 
168### The Framework Trap
169 
170Frameworks 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.
171 
172This is a trap. When the framework IS the architecture:
173- You cannot test business logic without the framework running
174- You cannot change the framework without rewriting the application
175- Framework bugs become your bugs, in your most critical code
176- Framework upgrades force changes throughout the system
177- Your code becomes an accessory to the framework rather than the framework serving your code
178 
179### Frameworks Want Marriage, You Want a Fling
180 
181Frameworks 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:
182 
183- Inherit from their base classes
184- Put their annotations on your code
185- Store your data in their preferred format
186- Structure your project their way
187 
188Each of these is a coupling point. The more you comply, the harder it is to separate.
189 
190**The Clean Architecture approach:**
191- Don't derive business objects from framework base classes
192- Don't put framework annotations on domain entities
193- Don't let the framework dictate your project structure
194- Treat the framework as a tool in the outermost circle, not as the foundation
195 
196### Practical Framework Isolation
197 
198| Framework Feature | Coupled Approach | Decoupled Approach |
199|-------------------|-----------------|-------------------|
200| **Routing** | Business logic in route handlers | Route handlers call Controllers; Controllers call Use Cases |
201| **ORM** | Domain entities ARE ORM models | Separate domain entities; map to/from ORM models in gateways |
202| **Validation** | Framework validation decorators on entities | Validation in Use Case or domain layer using plain code |
203| **Dependency injection** | `@Inject` annotations on domain classes | Constructor injection; wiring in Main component |
204| **Configuration** | `Settings.get("key")` in business logic | Inject config values as constructor parameters |
205| **Logging** | Framework logger called directly in Use Cases | Inject a logger interface; implement with framework in outer circle |
206 
207## The Database Is a Detail
208 
209The 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.
210 
211### Why It Matters
212 
213When business rules know about the database:
214- Testing requires a database (slow, fragile tests)
215- Database schema changes ripple into business logic
216- Migrating to a different database means rewriting business rules
217- The data model is driven by database capabilities rather than business needs
218 
219### Repository Pattern
220 
221The repository pattern is the primary mechanism for keeping the database at arm's length:
222 
2231. **Define the interface in the Use Case circle** -- it describes WHAT operations the business needs, not HOW data is stored
2242. **Implement the interface in the Adapter circle** -- this is where SQL, ORM calls, and database-specific code live
2253. **Inject the implementation at startup** -- Main wires the concrete repository into the use case
226 
227### ORM Considerations
228 
229ORMs are useful tools, but they must be contained in the outer circles:
230 
231**The two-model approach:**
232- **Domain model**: Pure business entities with business methods and rules. No ORM annotations. Lives in the Entity circle.
233- **Persistence model**: ORM-annotated classes that map to database tables. Lives in the Adapter circle. The gateway maps between the two.
234 
235This 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.
236 
237## The Web Is a Detail
238 
239The 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.
240 
241### Delivery Mechanism Independence
242 
243When use cases are independent of the delivery mechanism, you can:
244- Serve the same business logic through REST, GraphQL, gRPC, CLI, and WebSocket simultaneously
245- Test business logic without HTTP
246- Migrate from one web framework to another by rewriting only the outer circle
247 
248### Multiple Delivery Mechanisms
249 
250```
251REST Controller ----\
252 \
253GraphQL Resolver ------> Use Case Interactor ---> Entity
254 /
255CLI Command --------/
256Message Handler ---/
257```
258 
259Each 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.
260 
261## Plugin Architecture
262 
263The 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.
264 
265### How Plugins Work
266 
2671. **The core defines interfaces** (ports) that describe what it needs from the outside world
2682. **Plugins implement those interfaces** using specific technologies
2693. **Main assembles the plugins** and injects them into the core at startup
2704. **The core never knows which plugins are attached** -- it only knows the interfaces
271 
272### The Main Component
273 
274Main 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.
275 
276```python
277# main.py -- the composition root
278def create_app():
279 # Concrete infrastructure
280 db_pool = create_connection_pool(os.environ["DATABASE_URL"])
281 email_client = SendGridClient(os.environ["SENDGRID_API_KEY"])
282 
283 # Gateways (implement interfaces)
284 order_repo = PostgresOrderRepository(db_pool)
285 email_service = SendGridEmailService(email_client)
286 
287 # Presenters
288 order_presenter = JsonOrderPresenter()
289 
290 # Use Cases (wired with concrete dependencies)
291 place_order = PlaceOrderInteractor(order_repo, order_presenter)
292 cancel_order = CancelOrderInteractor(order_repo, email_service, order_presenter)
293 
294 # Controllers (wired with use cases)
295 order_controller = OrderController(place_order, cancel_order)
296 
297 # Framework wiring
298 app = Flask(__name__)
299 app.route("/orders", methods=["POST"])(order_controller.create)
300 app.route("/orders/<id>/cancel", methods=["POST"])(order_controller.cancel)
301 
302 return app
303```
304 
305Main 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.
306 
307### Plugin Swappability in Practice
308 
309| Plugin | Interface | Implementation A | Implementation B |
310|--------|-----------|-----------------|-----------------|
311| **Persistence** | `OrderRepository` | `PostgresOrderRepository` | `DynamoOrderRepository` |
312| **Email** | `EmailService` | `SendGridEmailService` | `SesEmailService` |
313| **Payment** | `PaymentGateway` | `StripeGateway` | `BraintreeGateway` |
314| **Search** | `ProductSearch` | `ElasticsearchProductSearch` | `AlgoliaProductSearch` |
315| **Cache** | `CacheStore` | `RedisCacheStore` | `MemcachedCacheStore` |
316| **File storage** | `FileStore` | `S3FileStore` | `LocalFileStore` |
317 
318Each 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.
319 
320## Keeping Frameworks at Arm's Length
321 
322### The Wrapper Strategy
323 
324When a framework provides something useful but you don't want to couple to it directly, wrap it:
325 
326```python
327# Interface in inner circle
328class Clock(ABC):
329 @abstractmethod
330 def now(self) -> datetime:
331 pass
332 
333# Wrapper in outer circle
334class SystemClock(Clock):
335 def now(self) -> datetime:
336 return datetime.utcnow()
337 
338# Test double
339class FakeClock(Clock):
340 def __init__(self, fixed_time: datetime):
341 self._time = fixed_time
342 
343 def now(self) -> datetime:
344 return self._time
345```
346 
347Now 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.
348 
349### When NOT to Wrap
350 
351Not everything needs a wrapper. Apply the rule pragmatically:
352 
353- **Standard library types** (strings, lists, dates as data): Don't wrap. They are stable and ubiquitous.
354- **Utility functions with no side effects**: Don't wrap `math.ceil()` or `json.dumps()`.
355- **Anything with I/O or side effects** (database, network, filesystem, clock, random): Wrap it.
356- **Anything from a framework you might swap**: Wrap it.
357 
358The test is: "Would I need to mock this in a test?" If yes, wrap it behind an interface.
359 

Discussion

Alternatives

ImpeccableUse when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.Design & UI · Apache-2.0Apple designApple's approach to interface design and fluid, physical motion, translated for the web. Use when building or reviewing gesture-driven UI, spring animations, drag/swipe/sheet interactions, momentum and interruptible transitions, translucent materials and depth, typography (optical sizing, tracking, leading), reduced-motion, or the design foundations (feedback, spatial consistency, restraint) behind Apple-style interfaces.Design & UI · MITBuilding AnimationsBuild an animation from scratch, making the decisions in the order that determines whether it feels right — should it animate at all, what purpose, which tool, which properties, which curve and duration, how it interrupts, how it exits. Writes the implementation. Use when asked to animate something, add motion, make a component feel alive, or build a transition. For critiquing existing motion use review-animations; for auditing a whole codebase use improve-animations.Design & UI · MITBreakRenders a component you choose in every state and scenario on a temporary page and stress tests it.Design & UI · MIT