Clean
Architecture

For Web Developers

Nils Röhrig @ DWX 2026

Introduction

Nils Röhrig

Software Engineer at Loql.
Speaker, trainer, author.

Introduction

Agenda

01.Clean Architecture

core idea & fundamentals

02.Hexagonal Architecture

practical usage via ports-and-adapters

03.Why it Matters

benefits & challenges

04.Execution

bring it to life in code

What we're gonna talk about today

What is Clean Architecture?

Who has heard about it?

Who has understood everything about it?

Clean Architecture

As coined by Robert C. Martin

Diagram by Robert C. Martin on The Clean Code Blog

Clean Architecture

As coined by Robert C. Martin

An actionable idea that consolidates multiple architectural approaches.


  • Concentric Circles
  • Multiple Layers
  • Inverted Control Flow

Fundamental rules of the business.

The Business Core

Enterprise Business Rules & Entities

The stuff that generates value for the business.


  • Broadly applicable
  • Rarely changed
  • Shared everywhere
  • Convey no
    technical details

The Application Capabilities

Application Business RUles

Stuff that makes an application useful for
the business.


  • Define app-specific
    use cases
  • Apply business
    entities & rules
  • Still convey no
    technical details

The Technical Translators

INterface Adapters

Stuff that connects
the application
 to the
real world.


  • Translate between core and external systems
  • Build abstractions over the outer world
  • Convey the necessary
    technical details

The Technical Details

Frameworks & drivers

The external systems used by the application.


  • Databases, UI frameworks, service providers, ...
  • May or may not contain a lot of your own code
  • Technical in their
    very nature

The point is Separation of Concerns.


We want business and application rules to stay blissfully unaware of technical details.

So, What's the Point?

Clean Architecture

The Heart of the Matter

The Dependency RUle

The Heart of the Matter

The Dependency RUle

Source code dependencies 
can only ever point inwards!


  • The most important rule
  • Essential for decoupling
  • Requires abstraction

The Heart of the Matter

The Dependency RUle

Source code dependencies 
can only ever point inwards!


  • The most important rule
  • Essential for decoupling
  • Requires abstraction

Dependency
Inversion

Invert source code dependency directions.

The Wrong Direction

Dependency Inversion

The Right Direction

Dependency Inversion

The Abstraction

Dependency Inversion

The Composition

Dependency Inversion

Side Notes

Clean Architecture

Clean Architecture does not require exactly four layers!


  • The rules matter more
    than the count
  • Add layers if useful,
    remove if not
  • The principles provide
    the value, not the shape

Getting
Practical

Hexagonal Architecture

Also known as Ports-and-Adapters.

Hexagonal Architecture

As coined by Alistair Cockburn

Diagram by Tom Hombergs on Reflectoring.io

Inside the Hexagon

Hexagonal Architecture

Inside the hexagon lives the application core.


  • Business Objects
  • Business Rules
  • Use Cases

Drivers License

Hexagonal Architecture

On the left-hand egde live the input pOrts.


  • Exposed to the outside
  • Implement use cases
  • Allow outside to drive the application

Application Drivers

Hexagonal Architecture

Outside to the left live the input adapters.


  • Drive the application via input ports
  • Can be anything

Using External Systems

Hexagonal Architecture

On the right-hand egde live the Output pOrts.


  • Exposed to the outside
  • Describe Requirements
  • Allow inside to drive external systems

Adapting External Systems

Hexagonal Architecture

Outside to the right live the output adapters.


  • Driven by the app
  • Implement Requirements

Same Idea, Different Shape

Hexagonal Architecture vs. Clean Architecture

The Dependency Rule holds true!


  • Source dependencies point inwards
  • Inside is decoupled from outside

I prefer Cockburn's approach 
for a couple of reasons:


  • Less convoluted
  • Fewer layers
  • Easier to reason about
  • Easier to start with
  • Can grow later

My Personal Preference

Hexagonal Architecture vs. Clean Architecture

Benefits &
Challenges

Why it Matters

01.Attributes


  • True separation of concerns
  • Boundaries are enforced by design
  • The core declares its requirements
  • The Outer world merely fulfils them

Benefits & Challenges

Why it Matters

Benefits & Challenges

Why it Matters

02.Consequences

  • Portability: The core does not care about externalities – just add adapters
  • Stability: Changes on the outside have no effect on the inside
  • Optionality: Delay decisions to the last possible moment
  • Maintainability: Structure helps humans and machines alike; testing is very easy

Benefits & Challenges

Why it Matters

03.What to keep in mind

  • Adds a fair amount of boilerplate
  • Overkill for very small or simple
    CRUD applications
  • Boundaries are not always
    easily defined

How to Tackle a
Business Problem...

Domain Modelling

...with Hexagonal Architecture in mind!

01.Identify Participants

Start with the Business Needs

Domain Modelling

02.Find activities & Artifacts

03.Derive Domain Concepts

Case Study

Contract Approval Flow

01.Draft

Salesperson drafts a contract

03.Rework

Salesperson reworks draft

02.Review

Supervisor sends feedback

04.Sign-Off

Supervisor approves draft

05.Send

Supervisor sends Contract

The Salesperson

01.Identify Participants

The Customer

01.Identify Participants

The Sales Supervisor

01.Identify Participants

Contract Draft Creation

02.Find Activities & Artifacts

Review & Feedback

02.Find Activities & Artifacts

Contract Draft Rework

02.Find Activities & Artifacts

The Contract Sign-Off

02.Find Activities & Artifacts

The Contract Send-Off

02.Find Activities & Artifacts

Language Appears Naturally

03.Deriving Domain Concepts

Artifact Entities


  • Contract Draft
  • Feedback
  • Signed-Off Contract

Participant Entities


  • Salesperson
  • Sales Supervisor
  • Customer

Contract Lifecycle


  1. Draft
  2. Submitted for review
  3. Rework required
  4. Signed-off
  5. Sent to Customer

Code or it
didn't happen!

Application Core Overview

Show me the code

Employees

Show me the code

// Entities
type Salesperson = {
  //... employee attributes,
  role: "salesperson";
  servedSegment: CustomerSegment;
};

type SalesSupervisor = {
  //... employee attributes,
  role: "sales supervisor";
};

type Employee = Salesperson | SalesSupervisor;

// Rules
function isSalesperson(employee: Employee): employee is Salesperson {
  /* ... */
}

Customers

Show me the code

type CustomerSegment = {
  // ...customer segment attributes
};

type Customer = {
  // ...customer attributes
  segment: CustomerSegment;
};

Artifacts

Show me the code

type ContractDraft = {
  // ... draft details
  id: string;
  author: Employee;
  status: "draft" | "submitted" | "reviewed" | "reworked" | "signed-off";
  customer: Customer;
  details: ContractDraftDetails;
};

type Feedback = {
  // ... feedback details
  author: Employee;
  subject: ContractDraft;
};

type Contract = {
  // ... contract details,
  author: Employee;
  approver: Employee;
  customer: Customer;
};

Use Case Examples

Show me the code

Drafting a Contract


  • Triggered by salesperson
  • Authorizes user for the action
  • Validates input data
  • Stores new contract draft

Approving a contract draft


  • Triggered by sales supervisor
  • Verifies users authorization
  • Validates input data
  • Stores new contract
  • Updates contract draft

Draft Contract Use Case

Show me the code

type PortForDraftingContracts = (
  command: DraftContractCommand,
) => Promise<void>;

type DraftContractCommand = {
  author: Employee;
  customer: Customer;
  details: { /*...*/ };
};

const draftContract: PortForDraftingContracts = async ({
  author,
  customer,
  details,
}) => {
  /* ... */
};

Use Case Factory

Show me the code

function createDraftContractUseCase(
  dependencies: DraftContractDependencies,
): PortForDraftingContracts {
  return async function draftContract(cmd) {
    if (!isSalesperson(cmd.author) { 
        throw new AuthorizationError(...)
    }
    if (!isValidDraftContractCommand(cmd)) { 
      	throw new ValidationError(...)
    }

    await dependencies.contractDrafts.create({
      ...cmd,
      status: "draft",
    });
  };
}
type DraftContractDependencies = {
  contractDrafts: PortForManagingContractDrafts;
};

type ContractDraftInsertDTO = {/*...*/};

type PortForManagingContractDrafts = {
  create(
  	contractDraftInsert: ContractDraftInsertDTO
  ): Promise<void>;
  findById(...): Promise<ContractDraft | undefined>;
};

A Note on Style

Show me the code

interface PortForDraftingContracts {
  execute(command: DraftContractCommand): Promise<void>;
}

class DraftContractUseCase implements PortForDraftingContracts {
  constructor(dependencies: DraftContractDependencies) {
    //...
  }

  async execute({ author, details }) {
    //...
  }
}

Approve Draft Use Case

Show me the code

function createApproveContractDraftUseCase(dependencies: {
  contractDrafts: PortForManagingContractDrafts;
  contracts: PortForManagingContracts;
}): PortForApprovingContractDrafts {
  return async function approveContractDraft({ approver, contractDraftId }) {
    if (!isSalesSupervisor()) { /* error */}

    const contractDraft = await dependencies.contractDrafts.findById(contractDraftId);

    if (!exists(contractDraft)) { /* error */ }
    if (!isSubmitted(contractDraft)) {/* error */}

    await dependencies.contractDrafts.update(contractDraftId, { status: "signed-off" })
    
    await dependencies.contracts.storeContract({ /* ... new contract details */ });
  };
}

It's called Ports-and-Adapters for a reason!


  • We have the business logic
  • We have defined ports
  • We do not have adapters yet

Adapting the Application

Show me the code

Implement Outgoing Ports

Show me the code

type PortForManagingContractDrafts = {
  create(
  	contractDraftInsert: ContractDraftInsertDTO
  ): Promise<void>;
  findById(...): Promise<ContractDraft | undefined>;
};

export function createSqliteContractDraftAdapter(dependencies: {
  db: Database;
}): PortForManagingContractDrafts {
  return {
    async create(contractDraftInsert) {
      dependencies.db
        .insert(contractDrafts)
        .values(toNewContractDraftRow(contractDraftInsert))
        .run();
    },
    async findById(contractDraftId) { /* ... */ },
  };
}

User-Fronting Systems

Show me the code

SalesFront


  • Fullstack SvelteKit
  • Frontend for Salespeople
  • Plugs into Drafting Use Case

BossMove


  • Ancient Desktop App
  • Connects to Express API
  • Frontend for Supervisors
  • Plugs into Approval Use Case

SalesFront Remote Function

Show me the code

import { form, getRequestEvent } from "$app/server";

const createContractDraft = form(
  DraftContractInputDTOSchema, // https://standardschema.dev/ compliant
  async ({ customerId, title, ...rest }) => {
    const { locals } = getRequestEvent();

    try {
      await locals.draftContract({
        author: locals.currentEmployee,
        customer: await locals.loadCustomer(customerId),
        details: { title, ...rest },
      });
      return { success: true };
    } catch (e) {
      return { success: false, error: e.message };
    }
  },
);

SalesFront Draft Form

Show me the code

<script lang="ts">
	import { createContractDraft } from 'contractDrafts.remote.ts';
</script>

<h1>Create a new contract draft</h1>

<form {...createContractDraft}>
	<!-- form content goes here -->

	<button>Publish!</button>
</form>

BossMove Route

Show me the code

const databaseClient = createDatabaseClient({ url: ".bossmove.db" });

const container = createContainer().register({
  db: asValue(databaseClient.db),
  contractDrafts: asFunction(createSqliteContractDraftAdapter),
  contracts: asFunction(createSqliteContractAdapter),
  approveContractDraft: asFunction(createApproveContractDraftUseCase),
});

const app = express();

app.post("/contract-drafts/:id/approval", async (req, res) => {
  const approveContractDraft = container.resolve("approveContractDraft");
  await approveContractDraft({
    approver: req.user,
    contractDraftId: req.params.id,
  });
  res.send();
});

Where to Put What?

Show me the code

Folder structure is important (for readability)

- Alistair Cockburn in his talk “Hexagonal Architecture” in the version from 2025.


  • The application core lives in application
  • Adapters live in infrastructure
  • Lint rules verify that application does not import from infrastructure or other places

Finishing Up with a Little Recap.

An actionable set of ideas for structuring software.


  • Separate valuable business concerns from technical details
  • Dependency Rule: source code dependencies point inward
  • Hexagonal Architecture expresses the same core idea

Clean Architecture

Recap

Design the software from the inside out to realize a clean core.


  • The core describes its needs
  • The outer world fulfills them
  • Adapters make systems replaceable

Practical Application

Recap

Thank you very much!