Boundaries and boundary anatomy skill

Boundaries are the lines that separate software elements.

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

Use now

Files of Boundaries and boundary anatomy

wondelai/main1 file
boundaries.md
Show the full text432 lines

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
  2. Boundary Crossing
  3. The Humble Object Pattern
  4. Partial Boundaries
  5. Services as Boundaries
  6. Test Boundaries
  7. 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):

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

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

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

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

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

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

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

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.

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

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

1# Boundaries and Boundary Anatomy
2 
3Boundaries 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.
4 
5This 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.
6 
7 
8## Table of Contents
91. [Boundary Anatomy](#boundary-anatomy)
102. [Boundary Crossing](#boundary-crossing)
113. [The Humble Object Pattern](#the-humble-object-pattern)
124. [Partial Boundaries](#partial-boundaries)
135. [Services as Boundaries](#services-as-boundaries)
146. [Test Boundaries](#test-boundaries)
157. [The Main Component as a Plugin](#the-main-component-as-a-plugin)
16 
17---
18 
19## Boundary Anatomy
20 
21### What Is a Boundary?
22 
23A 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.
24 
25### The Structure of a Full Boundary
26 
27A full boundary has components on both sides, connected through polymorphism:
28 
29```
30[Client Side] [Boundary] [Implementation Side]
31 |
32Controller ----calls----> InputPort (interface)
33 |
34 Interactor (implements InputPort)
35 |
36 Interactor ----calls----> OutputPort (interface)
37 |
38 Presenter (implements OutputPort)
39```
40 
41**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.
42 
43### Boundary Components
44 
45| Component | Circle | Role |
46|-----------|--------|------|
47| **Input Port** | Use Case | Interface that defines what the use case accepts |
48| **Output Port** | Use Case | Interface that defines what the use case produces |
49| **Interactor** | Use Case | Implements Input Port; calls Output Port |
50| **Controller** | Adapter | Calls Input Port; translates from delivery mechanism |
51| **Presenter** | Adapter | Implements Output Port; translates to display format |
52| **Data Transfer Objects** | Use Case | Simple structures that carry data across the boundary |
53| **Gateway Interface** | Use Case | Abstraction for data persistence or external services |
54| **Gateway Implementation** | Adapter | Concrete persistence or service access |
55 
56## Boundary Crossing
57 
58### How Data Flows Across Boundaries
59 
60Data crosses boundaries as simple data structures -- DTOs, structs, or primitives. Never as framework objects, ORM entities, or complex objects that carry dependencies.
61 
62**Inbound crossing (Controller to Use Case):**
63 
64```python
65# Controller creates a simple DTO and passes it inward
66@dataclass(frozen=True)
67class TransferFundsRequest:
68 source_account_id: str
69 destination_account_id: str
70 amount: str # String to avoid float precision issues
71 currency: str
72 
73# Controller
74class TransferController:
75 def handle(self, http_body: dict) -> None:
76 request = TransferFundsRequest(
77 source_account_id=http_body["from"],
78 destination_account_id=http_body["to"],
79 amount=http_body["amount"],
80 currency=http_body["currency"],
81 )
82 self._transfer_use_case.execute(request)
83```
84 
85**Outbound crossing (Use Case to Presenter):**
86 
87```python
88# Use Case creates a response DTO and passes it outward through the Output Port
89@dataclass(frozen=True)
90class TransferFundsResponse:
91 transfer_id: str
92 new_source_balance: str
93 timestamp: str
94 
95# In the Interactor:
96response = TransferFundsResponse(
97 transfer_id=transfer.id,
98 new_source_balance=str(source_account.balance),
99 timestamp=transfer.created_at.isoformat(),
100)
101self._presenter.present_success(response)
102```
103 
104### Flow of Control vs. Direction of Dependency
105 
106This is a subtle but critical distinction:
107 
108- **Flow of control:** Controller --> Interactor --> Presenter (left to right, outward at the end)
109- **Source code dependency:** Controller --> InputPort <-- Interactor --> OutputPort <-- Presenter
110 
111The 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.
112 
113## The Humble Object Pattern
114 
115### The Problem
116 
117Some 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:
118 
1191. **The Humble Object:** Contains the hard-to-test code, stripped of all logic. It's so simple that testing is unnecessary (or trivially easy).
1202. **The Testable Object:** Contains all the logic, extracted from the hard-to-test context so it can be tested in isolation.
121 
122### Pattern Structure
123 
124```
125[Testable Logic] [Humble Object]
126PresenterLogic -produces-> ViewModel
127(easy to test) (simple data)
128 |
129 v
130 View/Template
131 (hard to test, but so simple it doesn't matter)
132```
133 
134### Examples of Humble Objects
135 
136**1. View (GUI boundary):**
137 
138```python
139# Testable: Presenter that produces a ViewModel
140class OrderPresenterLogic:
141 def present(self, response: OrderResponse) -> OrderViewModel:
142 return OrderViewModel(
143 title=f"Order #{response.order_id}",
144 total=f"${response.total:.2f}",
145 status_color="green" if response.status == "completed" else "yellow",
146 items=[f"{i.name} x{i.qty}" for i in response.items],
147 )
148 
149# Humble: View that just renders the ViewModel (no logic to test)
150class OrderView:
151 def render(self, vm: OrderViewModel) -> str:
152 return self._template.render(vm) # Template rendering only
153```
154 
155The 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.
156 
157**2. Database Gateway (persistence boundary):**
158 
159```python
160# Testable: Use Case logic that decides what to persist
161class ApproveExpenseInteractor:
162 def execute(self, request: ApproveExpenseRequest) -> None:
163 expense = self._repo.find_by_id(request.expense_id)
164 expense.approve(request.approver_id) # Business logic -- testable
165 self._repo.save(expense)
166 
167# Humble: Repository that just maps and persists (minimal logic)
168class SqlExpenseRepository:
169 def save(self, expense: Expense) -> None:
170 self._conn.execute(
171 "UPDATE expenses SET status = %s, approved_by = %s WHERE id = %s",
172 (expense.status.value, expense.approver_id, expense.id),
173 )
174```
175 
176The Interactor contains the decision logic (testable with a mock repo). The Repository is humble -- it just maps entity state to SQL parameters.
177 
178**3. Service Gateway (external service boundary):**
179 
180```python
181# Testable: Logic that decides whether and how to send notifications
182class NotificationService:
183 def __init__(self, sender: NotificationSender):
184 self._sender = sender
185 
186 def notify_order_shipped(self, order: Order) -> None:
187 if order.customer_prefers_email():
188 self._sender.send_email(
189 to=order.customer_email,
190 subject=f"Order {order.id} shipped",
191 body=self._format_shipping_message(order),
192 )
193 
194# Humble: Just sends the message (hard to test, but no logic)
195class SmtpNotificationSender(NotificationSender):
196 def send_email(self, to: str, subject: str, body: str) -> None:
197 self._smtp.sendmail(self._from_addr, to, self._build_mime(subject, body))
198```
199 
200### Where Humble Objects Appear in Clean Architecture
201 
202| Boundary | Humble Object | Testable Partner |
203|----------|--------------|-----------------|
204| GUI/View | Template renderer, React component | Presenter logic that produces ViewModel |
205| Database | SQL execution, ORM save/load | Use Case logic, mapping logic |
206| External API | HTTP client wrapper | Service logic that decides what to send |
207| Filesystem | File read/write operations | Logic that decides what to read/write |
208| Clock/Random | System clock, random generator | Logic that uses injected clock/random |
209 
210## Partial Boundaries
211 
212### When Full Boundaries Are Too Expensive
213 
214Full 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.
215 
216### Three Forms of Partial Boundaries
217 
218**1. Skip the last step (prepare for full boundary later):**
219 
220Create 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.
221 
222```python
223# Same package, but clearly separated with interfaces
224# Can be split into separate packages later with minimal effort
225class OrderService:
226 def __init__(self, repo: OrderRepository): # Interface exists
227 self._repo = repo
228 
229class InMemoryOrderRepository(OrderRepository): # Implementation exists
230 ...
231 
232# Both live in the same package for now
233```
234 
235**2. Strategy pattern (one-sided boundary):**
236 
237```python
238# Only the outbound side has an interface
239class ReportGenerator:
240 def __init__(self, formatter: ReportFormatter):
241 self._formatter = formatter
242 
243 def generate(self, data: ReportData) -> str:
244 # Logic here
245 return self._formatter.format(processed_data)
246 
247class PdfFormatter(ReportFormatter):
248 def format(self, data) -> str: ...
249 
250class CsvFormatter(ReportFormatter):
251 def format(self, data) -> str: ...
252```
253 
254No Input Port, no Output Port -- just a simple strategy. Lighter weight than a full boundary.
255 
256**3. Facade pattern (simplest):**
257 
258```python
259class OrderFacade:
260 """Single entry point to order subsystem. Hides internal complexity."""
261 def place_order(self, items, customer_id):
262 # Delegates to internal classes
263 order = self._order_factory.create(items, customer_id)
264 self._order_repo.save(order)
265 self._notifier.notify(order)
266```
267 
268The Facade provides a simpler interface but doesn't enforce dependency direction. It's the weakest form of boundary -- better than nothing, but easily violated.
269 
270### Choosing Boundary Strength
271 
272| Situation | Boundary Type | Cost | Protection |
273|-----------|--------------|------|------------|
274| Will definitely need to swap implementations | Full boundary (ports on both sides) | High | Complete |
275| Might need to swap; want the option | Partial (interfaces, same package) | Medium | Good |
276| Multiple strategies but stable architecture | Strategy pattern | Low-medium | Moderate |
277| Just want to simplify access to a subsystem | Facade | Low | Minimal |
278| Uncertain -- need might never arise | None (but document the decision) | Zero | None |
279 
280## Services as Boundaries
281 
282### Services Are Not Inherently Architectural
283 
284A 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.
285 
286### When Services Create Real Boundaries
287 
288A service creates a genuine architectural boundary when:
289- It has its own data store that no other service accesses directly
290- It communicates through well-defined interfaces (API contracts)
291- Its internal structure follows the Dependency Rule independently
292- It can be developed, deployed, and scaled independently
293 
294### When Services Fail as Boundaries
295 
296| Anti-Pattern | Why It Fails |
297|-------------|-------------|
298| Shared database | Changes to the schema affect all services -- they're coupled |
299| Shared data model library | All services import the same DTOs -- they change together |
300| Synchronous orchestration | Service A calls B calls C calls D -- distributed monolith |
301| Chatty communication | Services exchange many small calls -- performance and coupling |
302 
303### Services Should Contain Clean Architecture
304 
305Each service should have its own concentric circles internally:
306 
307```
308Service Boundary
309├── Entities (domain objects for this service's bounded context)
310├── Use Cases (application logic for this service)
311├── Adapters (controllers, gateways, presenters for this service)
312└── Frameworks (HTTP server, database driver for this service)
313```
314 
315The service boundary is a deployment boundary. The Clean Architecture circles within each service are architectural boundaries. Both are needed.
316 
317## Test Boundaries
318 
319### Tests as the Most Isolated Component
320 
321Tests 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.
322 
323### The Testing Boundary Structure
324 
325```
326[Production Code] [Test Code]
327Entity ---------<depends-on------ EntityTest
328UseCase --------<depends-on------ UseCaseTest
329Adapter --------<depends-on------ AdapterTest
330 
331(No arrow from Production to Test)
332```
333 
334### Testing Each Circle
335 
336| Circle | Test Strategy | Dependencies Needed |
337|--------|--------------|-------------------|
338| **Entities** | Unit tests with no mocks | None -- entities are self-contained |
339| **Use Cases** | Unit tests with mocked ports | Mock repositories, mock presenters |
340| **Adapters** | Integration tests | Real database (testcontainers), real HTTP |
341| **Frameworks** | End-to-end tests | Full system running |
342 
343### The Fragile Test Problem
344 
345When 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.
346 
347```python
348# FRAGILE: Test depends on internal implementation
349def test_order_internal_state():
350 order = Order(items)
351 assert order._internal_state == "pending" # Private field -- fragile
352 
353# ROBUST: Test depends on public behavior (same interface as production code)
354def test_order_is_pending_after_creation():
355 order = Order(items)
356 assert order.status == OrderStatus.PENDING # Public behavior -- stable
357```
358 
359## The Main Component as a Plugin
360 
361### Main Is the Dirtiest Component
362 
363The 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.
364 
365**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.
366 
367### Main's Responsibilities
368 
3691. **Instantiate concrete infrastructure** (database connections, API clients, caches)
3702. **Instantiate concrete adapters** (repositories, presenters, gateways)
3713. **Instantiate use case interactors** with injected dependencies
3724. **Instantiate controllers** with injected use cases
3735. **Configure the framework** (routes, middleware, error handlers)
3746. **Start the application** (listen on port, begin event loop)
375 
376### Different Mains for Different Configurations
377 
378Because Main is a plugin, you can have multiple Main configurations:
379 
380```python
381# main_production.py
382def create_app():
383 repo = PostgresOrderRepository(production_db_pool)
384 emailer = SendGridEmailer(production_api_key)
385 ...
386 
387# main_test.py
388def create_app():
389 repo = InMemoryOrderRepository()
390 emailer = FakeEmailer()
391 ...
392 
393# main_local.py
394def create_app():
395 repo = SqliteOrderRepository("local.db")
396 emailer = ConsoleEmailer() # Prints to stdout
397 ...
398```
399 
400The 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.
401 
402### Main and Dependency Injection Frameworks
403 
404DI frameworks (Spring, Guice, tsyringe) can help wire dependencies in Main. But be careful:
405 
406- **Use DI framework annotations ONLY in Main or configuration classes** -- never in entities or use cases
407- The DI framework is itself a framework detail -- it belongs in the outermost circle
408- You should be able to wire the entire system manually in a test without the DI framework
409- If removing the DI framework would require changes to business logic, you've coupled too tightly
410 
411### The Plugin Architecture Realized
412 
413When Main is the only place that knows about concrete implementations, the entire system becomes a plugin architecture:
414 
415```
416 Main (composition root)
417 / | | \
418 / | | \
419 PostgresRepo SendGrid Express Stripe
420 | | | |
421 v v v v
422 [OrderRepo] [Emailer] [HTTP] [Payment]
423 (interface) (interface) (route) (interface)
424 \ | | /
425 \ | | /
426 Use Case Interactors
427 |
428 Entities
429```
430 
431Entities 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.
432 

Discussion