Senior 3 min · June 25, 2026

Clean Architecture: Stop Writing Code That Dies When the Database Changes

Clean Architecture explained with real production patterns.

N
Naren Founder & Principal Engineer

20+ years shipping large-scale distributed systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
June 25, 2026
last updated
1,663
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Clean Architecture keeps your business logic independent of frameworks, databases, and UI. You achieve this by defining interfaces (ports) in your core domain and implementing them (adapters) in outer layers. The dependency inversion principle ensures outer layers depend on inner layers, not the other way around.

✦ Definition~90s read
What is Clean Architecture?

Clean Architecture is a software design philosophy that separates business rules from external concerns like databases, frameworks, and UIs. It enforces a strict dependency rule: source code dependencies point inward, toward high-level policies, never outward.

Think of Clean Architecture like a restaurant kitchen.
Plain-English First

Think of Clean Architecture like a restaurant kitchen. The chef (business logic) doesn't care if the ingredients come from a local farm or a warehouse — they just need a consistent supply. The menu (interface) defines what's available. The kitchen staff (adapters) fetch ingredients from wherever. If the supplier changes, you only swap the staff, not the chef or the menu. Your core recipes stay untouched.

You've seen it happen. A 'simple' database migration from MySQL to PostgreSQL turns into a three-month rewrite. Or swapping a payment provider requires touching every service layer. That's because your business logic is tangled with infrastructure. Clean Architecture fixes that by making your core code completely unaware of the outside world. After reading this, you'll be able to design systems where swapping a database, a UI framework, or an external API is a matter of days, not months. You'll understand the real-world trade-offs and when this architecture is overkill.

The Core Problem: Why Your Code Is Fragile

Most codebases start with a simple structure: Controller → Service → Repository. That works until you need to change something fundamental. The real issue is dependency direction. In a typical layered architecture, the service layer depends on the repository, which depends on the database driver. If you change the database, you change the repository, which forces changes in the service, and sometimes even the controller. Clean Architecture flips this: the domain defines interfaces (ports), and the infrastructure implements them (adapters). The domain never knows about the database. It only knows about its own interfaces. This is the Dependency Inversion Principle in action.

DependencyInversion.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// io.thecodeforge — System Design tutorial

// Domain layer: defines the port
public interface OrderRepository {
    Order findById(OrderId id);
    void save(Order order);
}

// Use case: depends on abstraction, not implementation
public class SubmitOrderUseCase {
    private final OrderRepository repo;
    private final PaymentGateway paymentGateway;

    public SubmitOrderUseCase(OrderRepository repo, PaymentGateway paymentGateway) {
        this.repo = repo;
        this.paymentGateway = paymentGateway;
    }

    public void execute(Order order) {
        // Business logic: validate, calculate total, etc.
        paymentGateway.charge(order.getTotal());
        repo.save(order);
    }
}

// Infrastructure layer: implements the port
public class PostgresOrderRepository implements OrderRepository {
    private final DataSource dataSource;

    @Override
    public Order findById(OrderId id) {
        // SQL query here
    }

    @Override
    public void save(Order order) {
        // SQL insert/update here
    }
}
Output
Compiles and runs. The use case has zero imports from infrastructure packages.
Production Trap: Leaking ORM Annotations
Never put JPA @Entity or Hibernate annotations in your domain entities. That ties your business logic to a specific ORM. Use plain POJOs in the domain and map to ORM entities in the infrastructure layer. I've seen a team spend a week migrating from Hibernate to jOOQ because @Entity was everywhere.
Clean Architecture Dependency Rule Flow THECODEFORGE.IO Clean Architecture Dependency Rule Flow How to protect your code from database changes Entities & Use Cases Business rules, no framework imports Interface Adapters Controllers, presenters, gateways Frameworks & Drivers DB, UI, external APIs Dependency Inversion Outer depends on inner abstractions Testable Core Mock outer layers, test business logic ⚠ Leaking DB details into use cases Keep all SQL/ORM code in interface adapters only THECODEFORGE.IO
thecodeforge.io
Clean Architecture Dependency Rule Flow
Clean Architecture

The Dependency Rule: What Goes Where

The dependency rule is simple: source code dependencies can only point inward. Nothing in an inner circle can know about something in an outer circle. Typically you have four layers: Entities (enterprise business rules), Use Cases (application business rules), Interface Adapters (controllers, presenters, gateways), and Frameworks & Drivers (DB, UI, external APIs). The inner layers define interfaces; outer layers implement them. This means your domain code never imports anything from Spring, Hibernate, or even Java's SQL packages. It's pure Java/Kotlin/C#. This makes it testable in isolation and immune to framework churn.

LayerStructure.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// io.thecodeforge — System Design tutorial

// Domain entity — no annotations, no framework imports
public class Order {
    private OrderId id;
    private Money total;
    private OrderStatus status;

    public void submit() {
        // Business rules: validate, calculate, etc.
        this.status = OrderStatus.SUBMITTED;
    }
}

// Use case — orchestrates domain logic
public class SubmitOrderUseCase {
    private final OrderRepository repo;
    private final PaymentGateway paymentGateway;

    public void execute(Order order) {
        order.submit();
        paymentGateway.charge(order.getTotal());
        repo.save(order);
    }
}

// Interface adapter — maps HTTP request to domain
@Controller
public class OrderController {
    private final SubmitOrderUseCase useCase;

    @PostMapping("/orders")
    public ResponseEntity<Void> submitOrder(@RequestBody OrderRequest request) {
        Order order = new Order(request.getItems());
        useCase.execute(order);
        return ResponseEntity.ok().build();
    }
}
Output
The controller has a dependency on the use case, which has dependencies only on domain interfaces. No domain class imports anything from Spring.
Senior Shortcut: Package Structure
Use a package-per-layer approach: com.myapp.domain, com.myapp.usecase, com.myapp.adapter.in.web, com.myapp.adapter.out.persistence. This enforces the dependency rule at the build level. Tools like ArchUnit can verify this automatically in CI.
Dependency Direction in Clean ArchitectureTHECODEFORGE.IODependency Direction in Clean ArchitectureSource code dependencies point inward onlyEntitiesEnterprise business rulesUse CasesApplication business rulesInterface AdaptersControllers, presenters, gatewaysFrameworks & DriversDB, UI, external APIs⚠ Outer layers depend on inner layers, never the reverseTHECODEFORGE.IO
thecodeforge.io
Dependency Direction in Clean Architecture
Clean Architecture

Real-World Example: A Checkout Service

Let's build a checkout service. The domain has entities like Cart, Order, and interfaces like PaymentGateway and InventoryService. The use case CheckoutUseCase orchestrates: it validates stock, calculates total, charges payment, and creates an order. The infrastructure implements these interfaces: StripePaymentGateway, PostgresInventoryService, KafkaEventPublisher. The web adapter is a Spring controller. If you swap Stripe for Adyen, you only change the StripePaymentGateway class. The use case never knows. This is the payoff.

CheckoutService.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// io.thecodeforge — System Design tutorial

// Domain
public class Cart {
    private List<Item> items;
    public Money calculateTotal() { /* ... */ }
}

public interface PaymentGateway {
    PaymentResult charge(Money amount);
}

public interface InventoryService {
    boolean isInStock(Item item, int quantity);
}

// Use case
public class CheckoutUseCase {
    private final PaymentGateway paymentGateway;
    private final InventoryService inventoryService;
    private final OrderRepository orderRepository;

    public Order execute(Cart cart) {
        for (Item item : cart.getItems()) {
            if (!inventoryService.isInStock(item, 1)) {
                throw new OutOfStockException(item);
            }
        }
        Money total = cart.calculateTotal();
        PaymentResult result = paymentGateway.charge(total);
        Order order = new Order(cart, result);
        orderRepository.save(order);
        return order;
    }
}

// Infrastructure
public class StripePaymentGateway implements PaymentGateway {
    @Override
    public PaymentResult charge(Money amount) {
        // Stripe SDK calls
    }
}

public class PostgresInventoryService implements InventoryService {
    @Override
    public boolean isInStock(Item item, int quantity) {
        // SQL query
    }
}
Output
The use case has zero imports from Stripe or Postgres. It's pure business logic.
Interview Gold: Why Not Just Use Interfaces?
Many teams use interfaces but still put them in the same package as implementations. That's not Clean Architecture. The interface must be owned by the domain (inner circle), not by the infrastructure. If the interface lives in the infrastructure package, you still have an outward dependency.

When Clean Architecture Breaks Down

Clean Architecture adds indirection. For small projects or prototypes, it's overkill. You'll spend more time defining interfaces and mapping objects than writing business logic. Also, if your team isn't disciplined, the architecture erodes quickly. I've seen projects where use cases started calling repository methods directly, bypassing interfaces. Another trap: over-engineering. Not every external dependency needs an interface. If you're never going to swap a logging library, don't abstract it. The rule of thumb: abstract only at the boundaries where change is likely — databases, external APIs, file systems. Don't abstract internal utilities.

OverkillExample.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — System Design tutorial

// Don't do this for a simple utility
public interface StringUtils {
    String capitalize(String s);
}

public class SimpleStringUtils implements StringUtils {
    @Override
    public String capitalize(String s) {
        return s.substring(0, 1).toUpperCase() + s.substring(1);
    }
}

// Just use a static method or a simple class. No need for abstraction.
Output
Over-engineering. This adds no value.
Never Do This: Abstracting Everything
I inherited a codebase with 200+ interfaces, each with exactly one implementation. Changing a method signature required updating the interface, the implementation, and all call sites. That's not Clean Architecture — that's ceremony. Only abstract at architectural boundaries.

Testing: The Real Win

The biggest practical benefit of Clean Architecture is testability. Your use cases depend on interfaces, so you can mock them in unit tests. No database, no HTTP server, no file system. Tests run in milliseconds. This means you can test complex business logic exhaustively without setting up infrastructure. Integration tests still exist, but they're fewer and focused on the adapter layer. This separation also makes it easy to run tests in parallel without conflicts.

UnitTestExample.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — System Design tutorial

@Test
public void checkout_should_charge_and_save_order() {
    // Given
    PaymentGateway paymentGateway = mock(PaymentGateway.class);
    InventoryService inventoryService = mock(InventoryService.class);
    OrderRepository orderRepository = mock(OrderRepository.class);
    CheckoutUseCase useCase = new CheckoutUseCase(paymentGateway, inventoryService, orderRepository);
    Cart cart = new Cart(List.of(new Item("SKU123", 1)));
    when(inventoryService.isInStock(any(), anyInt())).thenReturn(true);
    when(paymentGateway.charge(any())).thenReturn(new PaymentResult(true, "txn_123"));

    // When
    Order order = useCase.execute(cart);

    // Then
    verify(paymentGateway).charge(any());
    verify(orderRepository).save(any());
    assertEquals(OrderStatus.SUBMITTED, order.getStatus());
}
Output
Test passes in <100ms. No database, no network.
Senior Shortcut: Test Doubles in Domain
Testing: Clean vs. Traditional ArchitectureTHECODEFORGE.IOTesting: Clean vs. Traditional ArchitectureUnit test speed and isolation comparisonClean ArchitectureMock interfaces for use casesNo DB or HTTP neededTests run in millisecondsExhaustive logic coverageTraditional LayeredDepends on concrete reposRequires DB or stubsTests run in secondsHard to cover edge casesClean Architecture makes business logic fully testable in isolationTHECODEFORGE.IO
thecodeforge.io
Testing: Clean vs. Traditional Architecture
Clean Architecture

Common Mistakes and How to Avoid Them

Mistake 1: Annotating domain entities with JPA or Jackson annotations. Fix: Keep domain entities as plain objects. Create separate DTOs or ORM entities in the infrastructure layer and map between them. Mistake 2: Letting use cases return framework-specific types (e.g., ResponseEntity). Fix: Use cases should return domain objects or simple DTOs. The adapter layer handles HTTP concerns. Mistake 3: Circular dependencies between layers. Fix: Use dependency injection and ensure that inner layers never import outer layers. Tools like ArchUnit can enforce this.

MappingExample.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// io.thecodeforge — System Design tutorial

// Domain entity — no annotations
public class Order {
    private OrderId id;
    private Money total;
    private OrderStatus status;
}

// Infrastructure entity — JPA annotated
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
    @Id
    private UUID id;
    private BigDecimal total;
    private String status;

    public Order toDomain() {
        return new Order(new OrderId(id), new Money(total), OrderStatus.valueOf(status));
    }

    public static OrderJpaEntity fromDomain(Order order) {
        OrderJpaEntity entity = new OrderJpaEntity();
        entity.id = order.getId().getValue();
        entity.total = order.getTotal().getAmount();
        entity.status = order.getStatus().name();
        return entity;
    }
}
Output
Domain is clean. Infrastructure handles mapping.
The Classic Bug: Lazy Loading in Domain
If you pass a JPA proxy to a domain entity and try to access a lazy-loaded field outside a transaction, you get LazyInitializationException. Always map to a plain domain object before passing it to the use case.

Interview Questions That Actually Get Asked

  1. 'How does Clean Architecture handle cross-cutting concerns like logging or caching?' Answer: These are infrastructure concerns. Define interfaces in the domain (e.g., Logger) and implement them in infrastructure. Use decorators or AOP in the adapter layer. 2. 'When would you choose Clean Architecture over a simple layered architecture?' Answer: When the system has multiple external dependencies likely to change, or when business logic is complex and needs extensive unit testing. For CRUD apps with a single database, it's overkill. 3. 'What happens if a use case needs to call another use case?' Answer: Use cases should not depend on other use cases directly. Instead, compose them in a higher-level use case or use a mediator pattern. Direct dependency creates coupling.
CrossCuttingExample.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// io.thecodeforge — System Design tutorial

// Domain interface for logging
public interface Logger {
    void info(String message);
    void error(String message, Throwable t);
}

// Use case uses it
public class CheckoutUseCase {
    private final Logger logger;

    public void execute(Cart cart) {
        logger.info("Starting checkout for cart: " + cart.getId());
        // ...
    }
}

// Infrastructure implementation
public class Slf4jLogger implements Logger {
    private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Slf4jLogger.class);

    @Override
    public void info(String message) {
        log.info(message);
    }
}
Output
Logging is abstracted. You can swap SLF4J for Log4j without touching domain.
Interview Gold: Use Case Composition
If you have a 'place order' use case that needs to 'send email' and 'update inventory', don't make PlaceOrderUseCase depend on SendEmailUseCase. Instead, have PlaceOrderUseCase return an OrderResult, and let a higher-level orchestrator (or event bus) trigger the other use cases. This keeps each use case independently testable.
● Production incidentPOST-MORTEMseverity: high

The Payment Gateway Swap That Took 3 Months

Symptom
Switching from Stripe to Adyen required changes in 47 files across 6 modules. Regression tests failed for 2 weeks.
Assumption
Team assumed a simple adapter pattern would suffice.
Root cause
Payment logic was scattered across controllers, services, and even entity classes. No single interface defined the payment gateway contract. Every layer had direct imports of Stripe SDK classes.
Fix
Extracted a PaymentGateway interface in the domain layer. Created an AdyenPaymentGateway adapter. Moved all Stripe-specific code into a single StripePaymentGateway class. The swap took 2 days instead of 3 months.
Key lesson
  • If you can't swap a third-party dependency by changing one file, you don't have Clean Architecture — you have spaghetti.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
LazyInitializationException when accessing domain entity properties
Fix
1. Check if you're passing a JPA proxy to the use case. 2. Ensure mapping from JPA entity to domain entity happens before the transaction closes. 3. Use DTOs or projection interfaces to avoid lazy loading.
Symptom · 02
Circular dependency between modules at compile time
Fix
1. Run ArchUnit rule to detect cycles. 2. Move shared interfaces to a common domain module. 3. Ensure no domain module imports infrastructure modules.
Symptom · 03
Use case tests require Spring context to run
Fix
1. Check if use case constructor has dependencies that are not interfaces. 2. Ensure all external dependencies are injected via interfaces. 3. Use mocks or in-memory implementations in tests.
★ Clean Architecture Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
`LazyInitializationException` when reading a property
Immediate action
Check if the entity is a JPA proxy
Commands
System.out.println(entity.getClass().getName());
Check if the session is open: entityManager.contains(entity);
Fix now
Map JPA entity to domain entity before closing the transaction: Order domainOrder = jpaEntity.toDomain();
`NoSuchBeanDefinitionException` for a domain interface+
Immediate action
Check if the implementation is annotated with @Component or @Service
Commands
grep -r "implements OrderRepository" src/
Check if the package is scanned by Spring: @ComponentScan("com.myapp")
Fix now
Add @Repository to the implementation class or register it in a configuration class.
`ClassCastException` when mapping between layers+
Immediate action
Check the mapping method for type mismatches
Commands
grep -r "toDomain" src/
Check if fields are correctly mapped: compare field names and types.
Fix now
Use a mapping library like MapStruct or write explicit mapping with unit tests.
Build fails with `cyclic dependency` error+
Immediate action
Identify the cycle using dependency analysis
Commands
mvn dependency:tree -Dverbose | grep "cycle"
Check module dependencies in pom.xml or build.gradle.
Fix now
Extract the shared interface into a separate common module that both depend on.
Feature / AspectClean ArchitectureTraditional Layered Architecture
Dependency directionInward (domain knows nothing of infrastructure)Outward (service depends on repository)
TestabilityBusiness logic testable without infrastructureOften requires database or HTTP mocks
Change impactSwapping a database changes only adapter layerSwapping a database can ripple through all layers
ComplexityHigher initial overhead (interfaces, mapping)Lower initial overhead
Suitable forComplex business logic, multiple external dependenciesSimple CRUD, prototypes, small teams

Key takeaways

1
Clean Architecture is about dependency direction
domain defines interfaces, infrastructure implements them. Never the other way.
2
The real win is testability
business logic can be unit-tested in milliseconds without infrastructure.
3
Only abstract at architectural boundaries (DB, external APIs, file systems). Over-abstracting internal utilities adds ceremony without value.
4
If you can't swap a database or payment gateway by changing one file, you don't have Clean Architecture
you have spaghetti.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Clean Architecture handle cross-cutting concerns like logging o...
Q02SENIOR
When would you choose Clean Architecture over a simple layered architect...
Q03SENIOR
What happens if a use case needs to call another use case? How do you av...
Q04JUNIOR
What is the Dependency Inversion Principle and how does it apply to Clea...
Q05SENIOR
You're debugging a production issue where a use case is throwing a NullP...
Q06SENIOR
How would you design a system using Clean Architecture that needs to sup...
Q01 of 06SENIOR

How does Clean Architecture handle cross-cutting concerns like logging or caching without violating the dependency rule?

ANSWER
Define interfaces (ports) in the domain layer for cross-cutting concerns. Implement them in the infrastructure layer. The use case depends on the interface, not the implementation. For caching, use a decorator pattern around the repository interface in the infrastructure layer.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is Clean Architecture in simple terms?
02
What's the difference between Clean Architecture and Hexagonal Architecture?
03
How do I start implementing Clean Architecture in an existing project?
04
Does Clean Architecture work with microservices?
N
Naren Founder & Principal Engineer

20+ years shipping large-scale distributed systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
June 25, 2026
last updated
1,663
articles · all by Naren
🔥

That's Architecture. Mark it forged?

3 min read · try the examples if you haven't

Previous
Peer-to-Peer (P2P) Architecture
17 / 17 · Architecture
Next
SSO and SAML