DEV Community

Shuwen
Shuwen

Posted on

Clean Architecture Design Flow: A Practical Guide to Diagrams That Actually Help

When I applied Clean Architecture in a new project, I treated it as an engineering process, not a refactor-after-the-fact exercise.

Instead of starting with frameworks or package structures, I began by explicitly identifying the system’s use cases and modeling their workflows at a high level. These early diagrams helped me reason about execution flow, failure paths, and—most importantly—where architectural boundaries should live.

Only after those boundaries were clear did I open IntelliJ and start coding.

This allowed architecture to guide implementation, instead of being constantly corrected by it.


What I Did (Concrete Steps)

  • Identified and named core application use cases (business workflows)
  • Drew use case and sequence diagrams to understand execution flow
  • Used diagrams to identify core vs. infrastructure boundaries
  • Created the project skeleton in IntelliJ based on those boundaries
  • Defined interfaces (ports) in the core
  • Implemented adapters (REST, persistence, messaging) against ports
  • Updated high-level diagrams and shared them with the team

When to Draw Diagrams, What to Draw, and Why

If you’ve ever felt overwhelmed by UML diagrams that look impressive but don’t help you build better software, this guide is for you.

This is a lightweight, practical design flow for projects using Clean Architecture (Hexagonal Architecture). The goal is to make system intent explicit, reduce cognitive load, and keep diagrams useful—not decorative.


The Guiding Principle

Diagrams are thinking tools, not documentation artifacts.
Draw them to make decisions.
Stop drawing when the code becomes clearer than the diagram.


Stage 0 — Problem Framing (No Diagrams Yet)

Goal

Understand why the system exists.

What You Do

Write a short paragraph answering:

  • Who uses the system?
  • What problem does it solve?
  • What outcomes matter?

Example

This system handles order checkout, payment retries, and order expiration across REST APIs, Kafka retries, and scheduled jobs.

📌 No UML yet.
If this is unclear, diagrams won’t help.


Stage 1 — Identify Use Cases (MANDATORY)

Goal

Make system intent explicit.

What You Produce

  • A list of use cases (verbs, not nouns)
  • Optionally, a use case diagram

Input

  • Business requirements
  • Product discussions
  • Real workflows

Output

Clear, named use cases.

Example Use Case List

  • Checkout Order
  • Retry Payment
  • Expire Unpaid Orders

Use Case Diagram

┌──────────┐
│   User   │ ────────> Checkout Order
└──────────┘

┌──────────┐
│  Kafka   │ ────────> Retry Payment
└──────────┘

┌──────────┐
│Scheduler │ ────────> Expire Unpaid Orders
└──────────┘
Enter fullscreen mode Exit fullscreen mode

Rules

  • No entities
  • No methods
  • No databases
  • Only who triggers what

Stage 2 — Core Workflow (Sequence Diagram)

Goal

Understand flow and boundaries, not implementation.

When to Draw

  • A use case touches multiple systems
  • Failure handling matters

Input

  • One use case
  • Happy path (+ one failure path if needed)

Output

A simple sequence diagram.


Sequence Diagram: Checkout Order

Controller    CheckoutUseCase    InventoryPort    PaymentPort    OrderRepo    EventBus
    |                 |                |              |             |           |
    |-- execute(cmd)->|                |              |             |           |
    |                 |-- reserve() -->|              |             |           |
    |                 |<-- reserved ---|              |             |           |
    |                 |-- charge() ------------------>|             |           |
    |                 |<-- charged -------------------|             |           |
    |                 |-- save() ---------------------------------->|           |
    |                 |-- publish() ------------------------------------------>|
    |<-- result ------|                |              |             |           |
Enter fullscreen mode Exit fullscreen mode

Rules

  • Show ports, not implementations
  • No frameworks
  • No DTOs
  • Stop once decisions are clear

Stage 3 — Domain Modeling (Optional but Powerful)

Goal

Define business language and invariants.

When to Draw

  • Complex domain
  • Multiple developers
  • State transitions matter

Domain Model Diagram

┌─────────────────────────────┐
│           Order             │
├─────────────────────────────┤
│ id                          │
│ status                      │
│ total                       │
├─────────────────────────────┤
│ markPaid()                  │
│ expire()                    │
└─────────────────────────────┘
              |
              | 1..*
              ▼
┌─────────────────────────────┐
│        OrderItem            │
├─────────────────────────────┤
│ productId                  │
│ quantity                   │
│ price                      │
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Rules

  • No repositories
  • No controllers
  • No annotations
  • Only business meaning

Stage 4 — Architecture Skeleton (Folder Tree)

Purpose

Turn clear intent into enforceable structure before real code.

If someone opens your repo and only reads folder names,
they should understand what the system does.


Top-Level Boundaries

domain/
application/
adapter/
config/
Enter fullscreen mode Exit fullscreen mode

One Folder Per Use Case

application/usecase/
├── checkout/
├── retry/
└── expire/
Enter fullscreen mode Exit fullscreen mode

Use Case Placeholders

application/usecase/checkout/
├── CheckoutOrderUseCase.java
├── CheckoutOrderCommand.java
└── CheckoutOrderResult.java
Enter fullscreen mode Exit fullscreen mode

Rules

  • Minimal logic
  • No frameworks
  • No infrastructure imports

Ports (Interfaces Only)

application/port/out/
├── OrderRepositoryPort.java
├── PaymentPort.java
├── InventoryPort.java
└── EventPublisherPort.java
Enter fullscreen mode Exit fullscreen mode

Adapter Placeholders

adapter/
├── in/
└── out/
Enter fullscreen mode Exit fullscreen mode

Do not implement yet—this enforces direction.

orders-service/
├── README.md
├── docs/
│   ├── architecture/
│   │   ├── use-cases.md
│   │   ├── sequence-checkout-order.md
│   │   └── domain-model.md
│   └── decisions/
│       └── ADR-001-use-case-driven-design.md
│
├── src/main/java/com/example/orders/
│
│   ├── domain/
│   │   ├── model/
│   │   │   ├── Order.java
│   │   │   ├── OrderItem.java
│   │   │   ├── OrderStatus.java
│   │   │   ├── Money.java
│   │   │   └── Discount.java
│   │   │
│   │   ├── rule/
│   │   │   ├── PricingRule.java
│   │   │   └── OrderInvariant.java
│   │   │
│   │   └── event/
│   │       ├── OrderPaidEvent.java
│   │       └── OrderExpiredEvent.java
│
│   ├── application/
│   │   ├── usecase/
│   │   │   ├── checkout/
│   │   │   │   ├── CheckoutOrderUseCase.java
│   │   │   │   ├── CheckoutOrderCommand.java
│   │   │   │   ├── CheckoutOrderResult.java
│   │   │   │   └── CheckoutOrderService.java
│   │   │   │
│   │   │   ├── retry/
│   │   │   │   ├── RetryPaymentUseCase.java
│   │   │   │   ├── RetryPaymentCommand.java
│   │   │   │   └── RetryPaymentService.java
│   │   │   │
│   │   │   └── expire/
│   │   │       ├── ExpireUnpaidOrdersUseCase.java
│   │   │       └── ExpireUnpaidOrdersService.java
│   │   │
│   │   └── port/
│   │       ├── out/
│   │       │   ├── OrderRepositoryPort.java
│   │       │   ├── PaymentPort.java
│   │       │   ├── InventoryPort.java
│   │       │   ├── FraudCheckPort.java
│   │       │   ├── EventPublisherPort.java
│   │       │   ├── IdempotencyPort.java
│   │       │   └── ClockPort.java
│   │       │
│   │       └── in/
│   │           └── (optional - often implicit via usecase)
│
│   ├── adapter/
│   │   ├── in/
│   │   │   ├── web/
│   │   │   │   ├── OrderController.java
│   │   │   │   └── OrderRequestMapper.java
│   │   │   │
│   │   │   ├── messaging/
│   │   │   │   ├── CheckoutRetryConsumer.java
│   │   │   │   └── RetryMessageMapper.java
│   │   │   │
│   │   │   └── scheduler/
│   │   │       └── ExpireUnpaidOrdersJob.java
│   │   │
│   │   └── out/
│   │       ├── persistence/
│   │       │   ├── JpaOrderRepositoryAdapter.java
│   │       │   ├── JpaOrderEntity.java
│   │       │   └── SpringDataOrderRepository.java
│   │       │
│   │       ├── payment/
│   │       │   ├── StripePaymentAdapter.java
│   │       │   └── PaymentMapper.java
│   │       │
│   │       ├── inventory/
│   │       │   └── InventoryHttpAdapter.java
│   │       │
│   │       ├── fraud/
│   │       │   └── FraudApiAdapter.java
│   │       │
│   │       ├── messaging/
│   │       │   └── KafkaEventPublisherAdapter.java
│   │       │
│   │       └── idempotency/
│   │           └── RedisIdempotencyAdapter.java
│
│   └── config/
│       ├── UseCaseConfig.java
│       ├── AdapterConfig.java
│       └── ApplicationConfig.java
│
└── src/test/java/com/example/orders/
    ├── domain/
    │   └── OrderTest.java
    │
    ├── application/
    │   ├── checkout/
    │   │   └── CheckoutOrderServiceTest.java
    │   └── retry/
    │       └── RetryPaymentServiceTest.java
    │
    └── adapter/
        ├── in/
        │   └── web/
        │       └── OrderControllerTest.java
        └── out/
            └── persistence/
                └── JpaOrderRepositoryAdapterTest.java
Enter fullscreen mode Exit fullscreen mode

Stage 5 — Identify Ports

What Qualifies as a Port?

Anything that:

  • Touches a DB
  • Touches the network
  • Touches time
  • Sends messages
interface PaymentPort {
    PaymentResult charge(Money amount);
}
Enter fullscreen mode Exit fullscreen mode

📌 Ports emerge naturally.
If you’re forcing them, you started too early.


Stage 6 — Implement Adapters (No Diagrams)

What You Do

  • REST controllers
  • Kafka consumers
  • JPA repositories
  • External API clients

Rule

Adapters depend on the core — never the other way around.

📌 No UML here.


Stage 7 — Post-Implementation Diagrams (Optional)

Purpose

Onboarding and communication.

Draw:

  • One hexagonal overview
  • One key sequence diagram

Design Flow Cheat Sheet

Stage What to Draw Purpose
0 Nothing Clarify intent
1 Use cases Make intent explicit
2 Sequence diagram Find boundaries & flow
3 Domain model (optional) Shared language
4 Folder structure Lock architecture
5 Interfaces (ports) Define dependencies
6 Nothing Implement adapters
7 Overview diagrams Onboarding

Final Rule to Remember

If a new developer understands the system by reading use case names,
your architecture is working.

If they must jump across controllers, services, and repositories to understand intent—it’s not.


Key Takeaways

  • Draw diagrams to make decisions, not to look professional
  • Start with use cases — they reveal system intent
  • Sequence diagrams expose boundaries early
  • Domain models create shared language
  • Stop diagramming when code becomes clearer
  • Ports emerge naturally from workflows
  • Post-implementation diagrams are for onboarding, not design

Clean Architecture isn’t about rigid rules.
It’s about clarity of intent, clear boundaries, and maintainable code.

Start simple.
Draw only what helps you think.
Let architecture emerge from real use cases, not theoretical purity.

Top comments (0)