Advanced Software Architecture Masterclass. Implements Clean Architecture, Hexagonal (Ports & Adapters), and DDD for 2026 ecosystems (Next.js 16.2, React 19.3, Node.js 24)...
Master the art of building maintainable, scalable, and testable systems using proven architectural patterns. This skill is optimized for the Next.js 16.2 / React 19.3 / Node.js 24 ecosystem, focusing on decoupling business logic from infrastructure and enabling Autonomous AI Agents to work effectively within your codebase.
In 2026, software is increasingly built and refactored by AI Coding Agents. For an agent to be effective, the codebase must have predictable boundaries and explicit contracts.
The "Move Fast and Break Things" era has evolved into the "Move Fast with AI and Maintain Sanity" era. Without a solid architecture:
app router or server actions.In 2026, we design for two audiences: Humans and Agents.
The most common production stack in 2026 uses Next.js 16.2 with Server Functions, TypeScript 5.x, and Bun/pnpm monorepos.
src/
├── domain/ # Core Logic (The "Truth")
│ ├── entities/ # Identifiable objects with identity and lifecycle
│ ├── value-objects/# Data wrappers with validation (Email, Money)
│ ├── services/ # Pure logic involving multiple entities
│ ├── events/ # Domain Events (OrderPlaced, UserDeactivated)
│ └── exceptions/ # Custom domain exceptions
├── application/ # Orchestration (The "User's Intent")
│ ├── use-cases/ # Specific business actions (Commands)
│ ├── ports/ # Interfaces (contracts for Infrastructure)
│ ├── dtos/ # Data Transfer Objects
│ └── queries/ # Read-only operations (if using CQRS)
├── infrastructure/ # Tools (The "Implementation")
│ ├── adapters/ # Implementations of Ports (Prisma, Stripe, AWS)
│ ├── persistence/ # DB Schemas, Migrations, Seeders
│ └── external/ # Third-party API clients
├── presentation/ # UI (The "View")
│ ├── components/ # React 19 Server/Client components
│ ├── actions/ # Next.js Server Actions (Primary Adapters)
│ └── pages/ # Route Handlers and Page definitions
└── shared/ # Cross-cutting concerns (Constants, Utils)
The interaction model has changed. We no longer just "fetch from API". We "Invoke a Service".
Uses the new useActionState and useOptimistic for a seamless UX.
// presentation/components/ProjectForm.tsx
'use client'
import { useActionState } from 'react';
import { createProjectAction } from '../actions/project-actions';
export function ProjectForm() {
const [state, formAction, isPending] = useActionState(createProjectAction, null);
return (
<form action={formAction}>
<div className="flex flex-col gap-4">
<label htmlFor="projectName">Project Name</label>
<input
id="projectName"
name="projectName"
className="border p-2 rounded"
required
/>
<button
className="bg-blue-600 text-white p-2 rounded"
disabled={isPending}
>
{isPending ? 'Creating...' : 'Create Project'}
</button>
{state?.error && <p className="text-red-500">{state.error}</p>}
</div>
</form>
);
}
Acts as the Primary Adapter. It handles the transition from HTTP (FormData) to Application logic.
// presentation/actions/project-actions.ts
'use server'
import { CreateProjectUseCase } from '@/application/use-cases/CreateProject';
import { PrismaProjectRepository } from '@/infrastructure/adapters/PrismaProjectRepository';
export async function createProjectAction(prevState: any, formData: FormData) {
const name = formData.get('projectName') as string;
// Dependency Injection: In 2026, we prefer explicit composition over complex DI containers
const repo = new PrismaProjectRepository();
const useCase = new CreateProjectUseCase(repo);
try {
const project = await useCase.execute({ name });
return { success: true, data: project };
} catch (e) {
// Audit log or Error tracking integration
console.error("[Action Error]", e);
return { success: false, error: e.message };
}
}
Entities represent business concepts with identity.
// src/domain/entities/User.ts
import { Email } from '../value-objects/Email';
export class User {
constructor(
public readonly id: string,
public readonly email: Email,
private _isActive: boolean = true
) {}
public deactivate() {
this._isActive = false;
}
get isActive() { return this._isActive; }
}
Use Cases orchestrate the domain objects to perform a specific task.
// src/application/use-cases/DeactivateUser.ts
import { IUserRepository } from '../ports/IUserRepository';
export class DeactivateUserUseCase {
constructor(private userRepo: IUserRepository) {}
async execute(userId: string): Promise<void> {
// 1. Fetch from repository (Port)
const user = await this.userRepo.findById(userId);
if (!user) throw new Error("User not found");
// 2. Perform domain logic (Entity)
user.deactivate();
// 3. Persist changes (Port)
await this.userRepo.save(user);
}
}
The "Adapter" that talks to the database.
// src/infrastructure/adapters/PrismaUserRepository.ts
import { IUserRepository } from '@/application/ports/IUserRepository';
import { User } from '@/domain/entities/User';
import { Email } from '@/domain/value-objects/Email';
import { prisma } from '../prisma';
export class PrismaUserRepository implements IUserRepository {
async findById(id: string): Promise<User | null> {
const data = await prisma.user.findUnique({ where: { id } });
if (!data) return null;
return new User(
data.id,
Email.create(data.email),
data.isActive
);
}
async save(user: User): Promise<void> {
await prisma.user.upsert({
where: { id: user.id },
update: { isActive: user.isActive },
create: {
id: user.id,
email: user.email.value,
isActive: user.isActive
}
});
}
}
Don't use string for everything. Use Value Objects for validation and business logic.
// domain/value-objects/Money.ts
export class Money {
private constructor(public readonly amount: number, public readonly currency: string) {}
static create(amount: number, currency: string = 'USD'): Money {
if (amount < 0) throw new Error("Amount cannot be negative");
return new Money(amount, currency);
}
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error("Currency mismatch");
return new Money(this.amount + other.amount, this.currency);
}
}
An Aggregate ensures that business rules are always met across multiple related objects.
// domain/entities/Order.ts
export class Order {
private items: OrderItem[] = [];
addItem(product: Product, quantity: number) {
const totalItems = this.items.reduce((sum, item) => sum + item.quantity, 0);
if (totalItems + quantity > 50) {
throw new Error("An order cannot have more than 50 items");
}
this.items.push(new OrderItem(product.id, quantity));
}
}
One of the most common points of confusion in DDD.
TransferService between two bank accounts). It works with entities and value objects.Capture intent and decouple side-effects.
// domain/services/TransferService.ts
export class TransferService {
async execute(from: Account, to: Account, amount: Money) {
from.withdraw(amount);
to.deposit(amount);
// Domain Events are captured here
return new FundsTransferredEvent(from.id, to.id, amount);
}
}
In 2026, we don't start with "Tables". We start with "Events".
OrderPlaced, InventoryReduced).PlaceOrder).In 2026, we record why we made a choice, not just what we chose.
# ADR 005: Use Clean Architecture for AI Core
## Status
Accepted
## Context
We need a way for AI agents to refactor code safely without breaking business rules. The previous "Spaghetti Monolith" resulted in AI hallucinating database queries in React components.
## Decision
We will use Clean Architecture with strict separation between Domain and Infrastructure.
## Consequences
- Pros: Predictable file structure, high testability, zero framework leakage in domain.
- Cons: More boilerplate for simple CRUD operations, higher learning curve for junior devs.
How different parts of the system talk to each other.
In a Clean Architecture, you have different "Testing Grooves":
// tests/use-cases/DeactivateUser.test.ts
import { DeactivateUserUseCase } from '@/application/use-cases/DeactivateUser';
import { InMemoryUserRepository } from '../mocks/InMemoryUserRepository';
test('should deactivate user', async () => {
const repo = new InMemoryUserRepository();
await repo.save({ id: '1', email: 'test@test.com', isActive: true });
const useCase = new DeactivateUserUseCase(repo);
await useCase.execute('1');
const user = await repo.findById('1');
expect(user.isActive).toBe(false);
});
Use tools to ensure the "Dependency Rule" is never broken.
dependency-cruiser config (2026 best practice){
"forbidden": [
{
"name": "domain-not-import-infra",
"from": { "path": "src/domain" },
"to": { "path": "src/infrastructure" }
},
{
"name": "usecase-not-import-infra",
"from": { "path": "src/application" },
"to": { "path": "src/infrastructure" }
},
{
"name": "presentation-not-import-infra",
"from": { "path": "src/presentation" },
"to": { "path": "src/infrastructure" }
}
]
}
use cache directive in the presentation layer.IUserRepository).BAD: Using @prisma/client types as return types in your Domain or Application layers.
GOOD: Map database rows to Domain Entities in the Infrastructure layer.
BAD: Putting all your logic in UserService and having User as just a type.
GOOD: Move business rules (e.g., user.canJoinProject()) into the User class.
BAD: Putting everything in shared/ because you are too lazy to decide where it belongs.
GOOD: Keep shared/ minimal. If it's business-related, it's domain/. If it's tool-related, it's infrastructure/.
BAD: Splitting into 10 repos before you understand the domain boundaries. GOOD: Build a Modular Monolith using Clean Architecture first. Boundaries are easy to cut later if they are clean.
BAD: data, info, manager, process.
GOOD: PaymentRecord, UserAuthenticationDetails, OrderOrchestrator.
Explore our specialized documentation for deeper technical insights:
Before refactoring, always analyze the existing codebase structure.
# Optimized command for architecture discovery
npx repomix@latest --include "src/**/*.ts" --compress --output /tmp/arch-discovery.xml
Using Repomix allows you to "pack" your architecture into a single file that can be fed into an AI for a full audit.
Updated: January 22, 2026 - 15:18