For Web Developers
Nils Röhrig @ DWX 2026
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
Who has heard about it?
Who has understood everything about it?
As coined by Robert C. Martin
Diagram by Robert C. Martin on The Clean Code Blog
As coined by Robert C. Martin
An actionable idea that consolidates multiple architectural approaches.
Fundamental rules of the business.
Enterprise Business Rules & Entities
The stuff that generates value for the business.
Application Business RUles
Stuff that makes an application useful for
the business.
INterface Adapters
Stuff that connects
the application to the
real world.
Frameworks & drivers
The external systems used by the application.
The point is Separation of Concerns.
We want business and application rules to stay blissfully unaware of technical details.
Clean Architecture
The Dependency RUle
The Dependency RUle
Source code dependencies
can only ever point inwards!
The Dependency RUle
Source code dependencies
can only ever point inwards!
Invert source code dependency directions.
Photo by Vidar Nordli-Mathisen on Unsplash
Dependency Inversion
Dependency Inversion
Dependency Inversion
Dependency Inversion
Clean Architecture
Clean Architecture does not require exactly four layers!
Hexagonal Architecture
Also known as Ports-and-Adapters.
As coined by Alistair Cockburn
Diagram by Tom Hombergs on Reflectoring.io
Hexagonal Architecture
Inside the hexagon lives the application core.
Hexagonal Architecture
On the left-hand egde live the input pOrts.
Hexagonal Architecture
Outside to the left live the input adapters.
Hexagonal Architecture
On the right-hand egde live the Output pOrts.
Hexagonal Architecture
Outside to the right live the output adapters.
Hexagonal Architecture vs. Clean Architecture
The Dependency Rule holds true!
I prefer Cockburn's approach
for a couple of reasons:
Hexagonal Architecture vs. Clean Architecture
Why it Matters
01.Attributes
Why it Matters
Why it Matters
02.Consequences
Why it Matters
03.What to keep in mind
Domain Modelling
...with Hexagonal Architecture in mind!
01.Identify Participants
Domain Modelling
02.Find activities & Artifacts
03.Derive Domain Concepts
Case Study
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
01.Identify Participants
01.Identify Participants
01.Identify Participants
02.Find Activities & Artifacts
02.Find Activities & Artifacts
02.Find Activities & Artifacts
02.Find Activities & Artifacts
02.Find Activities & Artifacts
03.Deriving Domain Concepts
Artifact Entities
Participant Entities
Contract Lifecycle
Show me the code
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 {
/* ... */
}Show me the code
type CustomerSegment = {
// ...customer segment attributes
};
type Customer = {
// ...customer attributes
segment: CustomerSegment;
};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;
};Show me the code
Drafting a Contract
Approving a contract draft
Show me the code
type PortForDraftingContracts = (
command: DraftContractCommand,
) => Promise<void>;
type DraftContractCommand = {
author: Employee;
customer: Customer;
details: { /*...*/ };
};
const draftContract: PortForDraftingContracts = async ({
author,
customer,
details,
}) => {
/* ... */
};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>;
};Show me the code
interface PortForDraftingContracts {
execute(command: DraftContractCommand): Promise<void>;
}
class DraftContractUseCase implements PortForDraftingContracts {
constructor(dependencies: DraftContractDependencies) {
//...
}
async execute({ author, details }) {
//...
}
}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!
Show me the code
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) { /* ... */ },
};
}Show me the code
SalesFront
BossMove
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 };
}
},
);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>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();
});Show me the code
Folder structure is important (for readability)
- Alistair Cockburn in his talk “Hexagonal Architecture” in the version from 2025.
applicationinfrastructureapplication does not import from infrastructure or other placesAn actionable set of ideas for structuring software.
Recap
Design the software from the inside out to realize a clean core.
Recap
Thank you very much!