Skip to content
· 15 min read

Clean Architecture Part 5: Architecture Implementation

Practical implementation of clean architecture covering presenters, humble objects, partial boundaries, the main component, services, testing, and embedded systems.

Clean Architecture Part 5: Architecture Implementation

This part covers the practical implementation of clean architecture: how to handle presentation, when to use partial boundaries, the role of the main component, the truth about services, testing strategies, and applying clean architecture to embedded systems.


Chapter 23: Presenters and Humble Objects

The Humble Object Pattern

The Humble Object Pattern separates hard-to-test behaviors from easy-to-test behaviors.

Example: GUIs are hard to test. Behavior that controls the GUI is easy to test. Separate them:

Presenters and Views

Presenter: Accepts data from the application and formats it for presentation.

public class OrderPresenter {
    public OrderViewModel present(OrderResponse response) {
        return new OrderViewModel(
            formatCurrency(response.total),
            formatDate(response.orderDate),
            response.items.stream()
                .map(this::formatItem)
                .collect(toList())
        );
    }
}

View: Takes the ViewModel and displays it. No logic—just rendering.

public class OrderView {
    public void render(OrderViewModel vm) {
        // Just puts strings on screen
        totalLabel.setText(vm.formattedTotal);
        dateLabel.setText(vm.formattedDate);
        itemList.setItems(vm.formattedItems);
    }
}

The View is “humble”—it’s so simple it doesn’t need testing.

Testing and Architecture

Humble objects appear at architectural boundaries:

Humble Object Summary

At every architectural boundary:

This makes the system testable while keeping the architecture clean.


Chapter 24: Partial Boundaries

Full architectural boundaries are expensive:

Sometimes you anticipate needing a boundary but don’t want to pay the full cost yet.

Skip the Last Step

Implement all the interfaces and data structures, but keep everything in the same component:

[Component]
├── UseCase
├── UseCaseInterface  ← boundary exists
├── Repository
└── RepositoryInterface  ← boundary exists

The code is structured for separation, but deployment is together. Later, splitting is easy.

Risk: Without separate compilation, discipline may erode. Developers might bypass the interfaces.

One-Dimensional Boundaries

Full boundaries have interfaces on both sides. A simpler approach uses the Strategy pattern—interface on only one side:

[Client] → [ServiceBoundary Interface] ← [ServiceImpl]

The client depends on the interface. The implementation can be swapped. But no interface protects the service from the client.

Useful when: You need to swap implementations but clients are stable.

Facades

The simplest partial boundary. A Facade class provides methods that call the underlying services:

public class ServiceFacade {
    private Service1 service1;
    private Service2 service2;

    public void operation1() { service1.op(); }
    public void operation2() { service2.op(); }
}

Trade-off: Client depends on the Facade, Facade depends on all services. The client is transitively coupled to all services.

Useful when: You want to simplify an API but don’t need true independence.

When to Use Partial Boundaries

Warning: Partial boundaries can degrade over time. If you never need the full boundary, the partial boundary is pure overhead.


Chapter 25: Layers and Boundaries

Hunt the Wumpus Example

Martin uses a text adventure game to explore boundaries:

First attempt: Separate game rules from UI

[Game Rules] → [UI Boundary] ← [Text UI]

But then: What if we want multiple languages? Add a language boundary:

[Game Rules] → [Language Boundary] ← [English]
                                   ← [Spanish]

And then: What about data storage? Game state persistence?

[Game Rules] → [Data Storage Boundary] ← [Flash Storage]
                                       ← [Cloud Storage]

The Architecture Keeps Evolving

Every time we think we have the boundaries right, a new requirement appears. The architecture must evolve.

Key insight: You can’t predict all boundaries upfront. Design for known requirements, watch for emerging boundaries.

Boundaries Everywhere?

You could put boundaries everywhere “just in case.” But:

Balance: Recognize where boundaries are likely. Don’t implement them until necessary, but keep the code structured so adding them is easy.

Watch for Warning Signs

Signs you might need a boundary:

When these appear, consider adding or strengthening a boundary.


Chapter 26: The Main Component

The Dirtiest Component

Every system has at least one component that creates, coordinates, and oversees the others. This is Main.

Main is the dirtiest component—it depends on everything else. It’s at the outermost circle of the clean architecture.

Main’s Responsibilities

  1. Create factories, strategies, and other global facilities
  2. Hand them to higher-level components via dependency injection
  3. Then transfer control to the abstract, clean components
public class Main {
    public static void main(String[] args) {
        // Create infrastructure
        Database db = new PostgresDatabase(config);
        EmailService email = new SmtpEmailService(config);

        // Create use case with injected dependencies
        OrderRepository repo = new SqlOrderRepository(db);
        OrderInteractor interactor = new OrderInteractor(repo, email);

        // Create and start the application
        WebServer server = new WebServer(interactor);
        server.start();
    }
}

Main as Plugin

Think of Main as a plugin to the application.

You might have:

Each Main configures the application differently, but the core architecture remains unchanged.

Configuration

Main handles configuration:

Then it translates configuration into dependencies and injects them.


Chapter 27: Services: Great and Small

The Service Illusion

Myth: Microservices are inherently good architecture.

Reality: Services are just another way to deploy components. They don’t automatically create good architecture.

Services Are Not Architecture

Two fallacies about services:

Fallacy 1: Services are decoupled

Services can be just as coupled as a monolith. If Service A breaks when Service B changes its API, they’re coupled.

Fallacy 2: Services support independent development

Large teams can work on a monolith with good component boundaries. Small teams can struggle with poorly-designed services.

The Problem with Coupling

Services often share:

Changes to shared elements affect all services. This is coupling, regardless of network boundaries.

The Value of Services

Services DO provide:

But these are operational benefits, not architectural benefits.

Architecture First, Services Second

Good monolith architecture > Bad service architecture

  1. Design good boundaries (interfaces, use cases, entities)
  2. Implement as a monolith with those boundaries
  3. Deploy as services IF operational requirements demand it

Don’t start with services hoping they’ll create good architecture.


Chapter 28: The Test Boundary

Tests Are Part of the System

Tests are not separate from the system—they’re part of its architecture. They must be designed with the same care as production code.

The Fragile Tests Problem

Tests that are coupled to volatile things break often:

Result: Teams disable or delete tests. Quality suffers.

Design for Testability

Create a testing API that decouples tests from volatile elements:

// Instead of testing via GUI
clickButton("submit");
assertTextDisplayed("Order confirmed");

// Test via API
OrderResponse response = orderApi.submitOrder(testOrder);
assertEquals("CONFIRMED", response.status);

The testing API:

Structural Coupling

Tests should not know about production code structure. If tests know about every class and function, refactoring becomes impossible.

Solution: Test through stable APIs (use cases, business rules), not through implementation details.

The Testing API

The testing API is a separate component:

[Tests] → [Testing API] → [Use Cases] → [Entities]

         [Test Utilities]
         [Test Database]

The Testing API might include:

Security Concerns

The Testing API creates a security risk—it bypasses normal access controls. Solutions:


Chapter 29: Clean Embedded Architecture

Embedded systems face unique challenges: limited resources, direct hardware access, real-time requirements. Clean architecture still applies.

The Problem

Many embedded systems are firmware—code tightly coupled to hardware. This makes code:

The Target-Hardware Bottleneck

If you can only test on target hardware:

Clean architecture separates software from hardware.

The Layers

Embedded systems need three layers:

1. Software Layer Application-specific business logic. Knows nothing about hardware.

2. Operating System Abstraction Layer (OSAL) Abstracts the OS. Provides threading, memory, timing services through interfaces.

3. Hardware Abstraction Layer (HAL) Abstracts the hardware. Provides sensor readings, actuator control through interfaces.

[Software (Business Rules)]

[Operating System Abstraction Layer]

[Hardware Abstraction Layer]

[Firmware & Hardware]

Example: LED Control

Without HAL:

// Directly manipulates hardware register
void turnOnLed() {
    *GPIO_PORT_A |= (1 << 5);
}

Can’t test without hardware. Can’t port to different hardware.

With HAL:

// Interface
void Led_TurnOn(Led led);
void Led_TurnOff(Led led);

// Implementation (can be swapped)
void Led_TurnOn(Led led) {
    *GPIO_PORT_A |= (1 << led.pin);
}

Now you can:

Benefits

  1. Testability: Test business logic without hardware
  2. Portability: Change hardware with minimal code changes
  3. Maintainability: Hardware experts work on HAL, software experts work on business logic
  4. Reusability: Business logic works on multiple hardware platforms

The Importance of Interfaces

Interfaces are even more important in embedded systems because hardware changes are common:

Investing in clean architecture pays dividends in embedded development.


Key Takeaways

  1. Humble Objects make testing possible. Put logic in testable components, keep hard-to-test components “humble.”

  2. Partial boundaries are a trade-off. Use them to prepare for future separation without paying full cost today.

  3. Main is the dirtiest component. It creates everything and injects dependencies. Think of it as a plugin.

  4. Services ≠ Architecture. Services provide operational benefits but don’t automatically create good architecture. Design boundaries first.

  5. Tests are architectural components. Design for testability. Create stable testing APIs. Avoid structural coupling.

  6. Embedded systems need clean architecture too. Hardware abstraction layers enable testing, portability, and maintainability.

  7. Boundaries evolve. You can’t predict all boundaries upfront. Design for what you know, watch for signals, and add boundaries when needed.


Resources