Clean Architecture principles for Modular Monolith with bounded contexts and minimal shared kernel. **ALWAYS use when working on backend code, ESPECIALLY when creating files, deciding file locations, or organizing contexts (auth, tax, bi, production).** Use proactively to ensure context isolation and prevent "Core Obesity Syndrome". Examples - "create entity", "add repository", "where should this file go", "modular monolith", "bounded context", "shared kernel", "context isolation", "file location", "layer organization".
You are an expert in Clean Architecture for Modular Monoliths. You guide developers to structure applications with isolated bounded contexts, minimal shared kernel ("anoréxico"), and clear boundaries following the principles: "Duplication Over Coupling", KISS, YAGNI, and "Start Ugly, Refactor Later".
You should proactively assist when:
NEVER use flat Clean Architecture (domain/application/infrastructure shared by all).
Instead, use isolated bounded contexts:
src/
├── contexts/ # Bounded contexts (NOT shared layers)
│ ├── auth/ # Complete vertical slice
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ │
│ ├── tax/ # Complete vertical slice
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ │
│ └── [other contexts]/
│
└── shared/ # Minimal shared kernel
└── domain/
└── value-objects/ # ONLY UUIDv7 and Timestamp!
Rule: Shared kernel must be minimal (< 5 files)
Before adding ANYTHING to shared/, it must pass ALL criteria:
Only allowed in shared:
uuidv7.value-object.ts - Universal identifiertimestamp.value-object.ts - Universal timestampPREFER code duplication over creating dependencies:
// ✅ GOOD: Each context has its own Money VO
// contexts/tax/domain/value-objects/tax-amount.ts
export class TaxAmount {
// Tax-specific implementation
}
// contexts/bi/domain/value-objects/revenue.ts
export class Revenue {
// BI-specific implementation
}
// ❌ BAD: Shared Money VO couples contexts
// shared/domain/value-objects/money.ts
export class Money {} // NO! Creates coupling
NEVER create base classes that couple contexts:
// ❌ BAD: Base class creates coupling
export abstract class BaseEntity {
id: string;
createdAt: Date;
// Forces all entities into same mold
}
// ✅ GOOD: Each entity is standalone
export class User {
// Only what User needs, no inheritance
}
Contexts communicate through Application Services, NEVER direct domain access:
// ✅ ALLOWED: Call through application service
import { AuthApplicationService } from "@auth/application/services/auth.service";
// ❌ FORBIDDEN: Direct domain import
import { User } from "@auth/domain/entities/user.entity"; // NEVER!
Rule: Dependencies must point inward, toward the domain
┌─────────────────────────────────────────┐
│ Infrastructure Layer │ ← External concerns
│ (DB, HTTP, Queue, Cache, External APIs)│ (Frameworks, Tools)
└────────────────┬────────────────────────┘
│ depends on ↓
┌────────────────▼────────────────────────┐
│ Application Layer │ ← Use Cases
│ (Use Cases, DTOs, Application Services)│ (Business Rules)
└────────────────┬────────────────────────┘
│ depends on ↓
┌────────────────▼─────────────────────────┐
│ Domain Layer │ ← Core Business
│ (Entities, Value Objects, Domain Rules) │ (Pure, Framework-free)
└──────────────────────────────────────────┘
Key Points:
IMPORTANT: These layers exist WITHIN each bounded context, not as shared layers.
Purpose: Pure business logic for this specific context
Location: contexts/[context-name]/domain/
Contains:
Rules:
Example Structure:
contexts/auth/domain/
├── entities/
│ ├── user.entity.ts
│ └── order.entity.ts
├── value-objects/
│ ├── email.value-object.ts
│ ├── money.value-object.ts
│ └── uuidv7.value-object.ts
├── ports/
│ ├── repositories/
│ │ ├── user.repository.ts
│ │ └── order.repository.ts
│ ├── cache.service.ts
│ └── logger.service.ts
├── events/
│ ├── user-created.event.ts
│ └── order-placed.event.ts
├── services/
│ └── pricing.service.ts
└── exceptions/
├── user-not-found.exception.ts
└── invalid-order.exception.ts
Key Concepts:
UUIDv7 is the recommended identifier for all entities. It provides:
Bun.randomUUIDv7() internally (available since Bun 1.3+)// domain/value-objects/uuidv7.value-object.ts
/**
* UUIDv7 Value Object (Generic)
*
* Generic UUID version 7 implementation that can be used by any entity.
*
* Responsibilities:
* - Generate time-ordered UUIDv7 identifiers
* - Validate UUID format
* - Provide type safety
* - Immutable by design
*
* Why UUIDv7?
* - Time-ordered: Monotonic, better database performance
* - Sequential writes: Optimal for B-tree indexes
* - Sortable: Natural ordering by creation time
* - Encodes: Timestamp + random value + counter
*
* Usage:
* Use as-is for entity identifiers:
* - UserId
* - OrderId
* - ProductId
* - etc.
*
* Available since Bun 1.3+
*/
export class UUIDv7 {
private readonly value: string;
private constructor(value: string) {
this.value = value;
}
/**
* Generates a new UUIDv7 identifier
*
* Uses Bun.randomUUIDv7() which generates time-ordered UUIDs.
*
* UUIDv7 features:
* - Time-ordered: Monotonic, suitable for databases
* - Better B-tree index performance (sequential insertion)
* - Sortable by creation time
* - Encodes timestamp + random value + counter
*
* Available since Bun 1.3+
*/
static generate(): UUIDv7 {
const uuid = Bun.randomUUIDv7();
return new UUIDv7(uuid);
}
/**
* Creates UUIDv7 from existing string
*
* Use when reconstituting from database or external source.
*
* @throws {Error} If UUID format is invalid
*/
static from(value: string): UUIDv7 {
if (!UUIDv7.isValid(value)) {
throw new Error(`Invalid UUID format: ${value}`);
}
return new UUIDv7(value);
}
/**
* Validates UUID format
*
* Accepts standard UUID format (v4, v7, etc.)
*/
private static isValid(value: string): boolean {
if (!value || typeof value !== "string") {
return false;
}
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
}
/**
* Compares two UUIDs for equality
*
* Value Objects are equal if their values are equal.
*/
equals(other: UUIDv7): boolean {
return this.value === other.value;
}
/**
* Returns string representation
*
* Use for serialization (database, JSON, logs).
*/
toString(): string {
return this.value;
}
/**
* Returns the raw value
*
* Use when you need the typed value explicitly.
*/
toValue(): string {
return this.value;
}
}
/**
* Type alias for User ID
*
* Use this type for all User entity ID references.
* This provides semantic clarity while using the generic UUIDv7 implementation.
*/
export type UserId = UUIDv7;
Usage in Entities:
// domain/entities/user.entity.ts
import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
import type { Email } from "@/domain/value-objects/email.value-object";
export class User {
private _isActive: boolean = true;
private readonly _createdAt: Date;
constructor(
private readonly _id: UUIDv7,
private _email: Email,
private _name: string,
private _hashedPassword: string
) {
this._createdAt = new Date();
}
deactivate(): void {
if (!this._isActive) {
throw new Error(`User ${this._id.toString()} is already inactive`);
}
this._isActive = false;
}
get id(): UUIDv7 {
return this._id;
}
get email(): Email {
return this._email;
}
get name(): string {
return this._name;
}
get isActive(): boolean {
return this._isActive;
}
get createdAt(): Date {
return this._createdAt;
}
}
Usage in Use Cases:
// application/use-cases/create-user.use-case.ts
import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
import { User } from "@/domain/entities/user.entity";
import { Email } from "@/domain/value-objects/email.value-object";
export class CreateUserUseCase {
async execute(dto: CreateUserDto): Promise<UserResponseDto> {
// Generate UUIDv7 for new user
const id = UUIDv7.generate();
const email = Email.create(dto.email);
const user = new User(id, email, dto.name, dto.hashedPassword);
await this.userRepository.save(user);
return {
id: user.id.toString(),
email: user.email.toString(),
name: user.name,
isActive: user.isActive,
createdAt: user.createdAt.toISOString(),
};
}
}
Usage in Repositories:
// infrastructure/repositories/user.repository.impl.ts
import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
import { User } from "@/domain/entities/user.entity";
export class UserRepositoryImpl implements UserRepository {
async findById(id: UUIDv7): Promise<User | null> {
const row = await this.db
.select()
.from(users)
.where(eq(users.id, id.toString()))
.limit(1);
if (!row) return null;
// Reconstruct domain entity
const userId = UUIDv7.from(row.id);
const email = Email.create(row.email);
return new User(userId, email, row.name, row.hashedPassword);
}
async save(user: User): Promise<void> {
await this.db.insert(users).values({
id: user.id.toString(),
email: user.email.toString(),
name: user.name,
isActive: user.isActive,
createdAt: user.createdAt,
});
}
}
For complete implementation examples of Entities, Value Objects, and Repositories with Drizzle ORM, see backend-engineer skill
Purpose: Orchestrate business logic, implement use cases
Contains:
Rules:
Example Structure:
src/application/
├── use-cases/
│ ├── create-user.use-case.ts
│ ├── update-user-profile.use-case.ts
│ └── deactivate-user.use-case.ts
├── dtos/
│ ├── create-user.dto.ts
│ └── user-response.dto.ts
└── mappers/
└── user.mapper.ts
Use Case Responsibilities:
Port (Interface) Example:
// ✅ Port in Domain layer (domain/ports/repositories/user.repository.ts)
// NO "I" prefix
import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
export interface UserRepository {
save(user: User): Promise<void>;
findById(id: UUIDv7): Promise<User | undefined>;
findByEmail(email: string): Promise<User | undefined>;
}
// Implementation in Infrastructure layer
For complete Use Case examples with DTOs, Mappers, and orchestration patterns, see backend-engineer skill
Purpose: Implement technical details and external dependencies
Contains:
Rules:
Example Structure:
src/infrastructure/
├── controllers/
│ ├── user.controller.ts
│ ├── order.controller.ts
│ └── schemas/
│ ├── user.schema.ts
│ └── order.schema.ts
├── repositories/
│ ├── user.repository.impl.ts
│ └── order.repository.impl.ts
├── adapters/
│ ├── cache/
│ │ └── redis-cache.adapter.ts
│ ├── logger/
│ │ └── winston-logger.adapter.ts
│ └── queue/
│ ├── sqs-queue.adapter.ts
│ ├── localstack-sqs.adapter.ts
│ └── fake-queue.adapter.ts
├── http/
│ ├── server/
│ │ └── hono-http-server.adapter.ts
│ ├── middleware/
│ │ ├── auth.middleware.ts
│ │ ├── validation.middleware.ts
│ │ └── error-handler.middleware.ts
│ └── plugins/
│ ├── cors.plugin.ts
│ └── openapi.plugin.ts
├── database/
│ ├── drizzle/
│ │ ├── schema/
│ │ │ └── users.schema.ts
│ │ └── migrations/
│ └── connection.ts
└── container/
└── main.ts
Infrastructure Layer Responsibilities:
domain/ports/repositories/ using Drizzle ORMRepository Pattern:
// Port in domain/ports/repositories/user.repository.ts
import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
export interface UserRepository {
save(user: User): Promise<void>;
findById(id: UUIDv7): Promise<User | undefined>;
}
// Implementation in infrastructure/repositories/user.repository.impl.ts
export class UserRepositoryImpl implements UserRepository {
// Drizzle ORM implementation
}
For complete Repository and Adapter implementations with Drizzle ORM, Redis, and other infrastructure examples, see backend-engineer skill
Purpose: Handle HTTP requests, WebSocket connections, CLI commands
Location: infrastructure/http/
Contains:
Rules:
Example Structure:
src/infrastructure/http/
├── server/
│ └── hono-http-server.adapter.ts
├── controllers/
│ ├── user.controller.ts
│ └── order.controller.ts
├── schemas/
│ ├── user.schema.ts
│ └── order.schema.ts
├── middleware/
│ ├── auth.middleware.ts
│ └── error-handler.middleware.ts
└── plugins/
├── cors.plugin.ts
└── openapi.plugin.ts
Controller Responsibilities:
Controller Pattern (Self-Registering):
// infrastructure/http/controllers/user.controller.ts
/**
* UserController
*
* Infrastructure layer (HTTP) - handles HTTP requests.
* Thin layer that delegates to use cases.
*
* Pattern: Constructor Injection + Auto-registration
*/
import type { HttpServer } from "@/domain/ports/http-server";
import { HttpMethod } from "@/domain/ports/http-server";
import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case";
export class UserController {
constructor(
private readonly httpServer: HttpServer, // ✅ HttpServer port injected
private readonly createUserUseCase: CreateUserUseCase // ✅ Use case injected
) {
this.registerRoutes(); // ✅ Auto-register routes in constructor
}
private registerRoutes(): void {
// POST /users - Create new user
this.httpServer.route(HttpMethod.POST, "/users", async (context) => {
try {
const dto = context.req.valid("json"); // Validated by middleware
const user = await this.createUserUseCase.execute(dto);
return context.json(user, 201);
} catch (error) {
console.error("Error creating user:", error);
return context.json({ error: "Internal server error" }, 500);
}
});
}
}
HttpServer Port (Domain Layer):
// domain/ports/http-server.ts
export enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
PATCH = "PATCH",
}
export type HttpHandler = (context: unknown) => Promise<Response | unknown>;
export interface HttpServer {
route(method: HttpMethod, url: string, handler: HttpHandler): void;
listen(port: number): void;
}
Key Benefits:
For complete HttpServer implementation (Hono adapter), Zod validation, and middleware patterns, see backend-engineer skill
Use custom DI Container (NO external libraries like InversifyJS or TSyringe)
Constructor Injection:
// ✅ Use cases depend on abstractions (ports), not implementations
export class CreateUserUseCase {
constructor(
private readonly userRepository: UserRepository, // Port from domain/ports/
private readonly passwordHasher: PasswordHasher, // Port from domain/ports/
private readonly emailService: EmailService // Port from domain/ports/
) {}
async execute(dto: CreateUserDto): Promise<UserResponseDto> {
// Orchestrate domain logic using injected dependencies
}
}
Lifetimes:
For complete DI Container implementation with Symbol-based tokens, registration patterns, and Hono integration, see backend-engineer skill
// ✅ Easy to test - no dependencies
import { describe, expect, it } from "bun:test";
import { User } from "@/domain/entities/user.entity";
import { Email } from "@/domain/value-objects/email.value-object";
import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
describe("User Entity", () => {
it("should deactivate user", () => {
const userId = UUIDv7.generate();
const email = Email.create("user@example.com");
const user = new User(userId, email, "John Doe", "hashed_password");
user.deactivate();
expect(user.isActive).toBe(false);
});
it("should throw error when deactivating already inactive user", () => {
const userId = UUIDv7.generate();
const email = Email.create("user@example.com");
const user = new User(userId, email, "John Doe", "hashed_password");
user.deactivate();
expect(() => user.deactivate()).toThrow();
});
});
// ✅ Test use case with mocked ports
import { describe, expect, it, mock } from "bun:test";
import { CreateUserUseCase } from "@/application/use-cases/create-user.use-case";
describe("CreateUserUseCase", () => {
it("should create user successfully", async () => {
// Arrange - Mock dependencies
const mockRepository = {
save: mock(async () => {}),
findByEmail: mock(async () => undefined),
};
const mockPasswordHasher = {
hash: mock(async (password: string) => `hashed_${password}`),
};
const mockEmailService = {
sendWelcomeEmail: mock(async () => {}),
};
const useCase = new CreateUserUseCase(
mockRepository as any,
mockPasswordHasher as any,
mockEmailService as any
);
const dto = {
email: "test@example.com",
password: "password123",
name: "Test User",
};
// Act
const result = await useCase.execute(dto);
// Assert
expect(mockRepository.save).toHaveBeenCalledTimes(1);
expect(mockPasswordHasher.hash).toHaveBeenCalledWith("password123");
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledTimes(1);
expect(result.email).toBe("test@example.com");
});
});
// Port (Domain lay
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer