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
└──────────┘
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 ------| | | | |
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 │
└─────────────────────────────┘
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/
One Folder Per Use Case
application/usecase/
├── checkout/
├── retry/
└── expire/
Use Case Placeholders
application/usecase/checkout/
├── CheckoutOrderUseCase.java
├── CheckoutOrderCommand.java
└── CheckoutOrderResult.java
Rules
- Minimal logic
- No frameworks
- No infrastructure imports
Ports (Interfaces Only)
application/port/out/
├── OrderRepositoryPort.java
├── PaymentPort.java
├── InventoryPort.java
└── EventPublisherPort.java
Adapter Placeholders
adapter/
├── in/
└── out/
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
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);
}
📌 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)