plugins/jira-orchestrator/skills/clean-architecture/SKILL.md
This skill should be used when the user asks to "apply clean architecture", "follow SOLID principles", "add dependency injection", "separate layers", "use hexagonal/onion architecture", "domain-driven design", or needs guidance on structuring code into clean, testable, decoupled layers.
npx skillsauth add markus41/claude clean-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Comprehensive guide for implementing Clean Architecture, SOLID principles, and maintainable code structures.
Dependencies point inward. Inner layers must not know about outer layers.
┌─────────────────────────────────────────────────┐
│ External Layer (Web, CLI, GraphQL) │
│ ┌───────────────────────────────────────────┐ │
│ │ Infrastructure (Repos, Adapters, ORM) │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ Application (Use Cases, Services) │ │ │
│ │ │ ┌───────────────────────────────────┐ │ │ │
│ │ │ │ Domain (Entities, VOs, Services) │ │ │ │
│ │ │ └───────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Dependencies point INWARD
Business rules isolated from technical concerns:
// Entity with behavior
export class User {
constructor(
public readonly id: UserId,
private passwordHash: PasswordHash
) {}
changePassword(newPassword: Password, hasher: PasswordHasher): void {
this.passwordHash = hasher.hash(newPassword);
}
}
// Value Object - immutable, validated
export class Email {
private constructor(private readonly value: string) {}
static create(email: string): Email {
if (!this.isValid(email)) throw new InvalidEmailError(email);
return new Email(email.toLowerCase());
}
private static isValid(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
// Repository interface - defines contract
export interface UserRepository {
findById(id: UserId): Promise<User | null>;
save(user: User): Promise<void>;
}
Orchestrates domain objects for use cases:
export class CreateUserUseCase {
constructor(
private readonly userRepository: UserRepository,
private readonly passwordHasher: PasswordHasher
) {}
async execute(input: CreateUserInput): Promise<CreateUserOutput> {
const existing = await this.userRepository.findByEmail(
Email.create(input.email)
);
if (existing) throw new EmailAlreadyExistsError();
const user = new User(
UserId.generate(),
Email.create(input.email),
this.passwordHasher.hash(input.password),
new Date()
);
await this.userRepository.save(user);
return user.toDTO();
}
}
Implements interfaces from inner layers:
export class PostgreSQLUserRepository implements UserRepository {
constructor(private readonly db: Database) {}
async findById(id: UserId): Promise<User | null> {
const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id.toString()]);
return row ? this.toDomain(row) : null;
}
async save(user: User): Promise<void> {
await this.db.query(
`INSERT INTO users (id, email, password_hash) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET email = $2`,
[user.id.toString(), user.email.toString(), user.passwordHash]
);
}
private toDomain(row: UserRow): User {
return new User(UserId.fromString(row.id), PasswordHash.fromString(row.password_hash));
}
}
Entry points to the application:
export class UserController {
constructor(private readonly createUserUseCase: CreateUserUseCase) {}
async create(req: Request, res: Response): Promise<void> {
try {
const result = await this.createUserUseCase.execute(req.body);
res.status(201).json(result);
} catch (error) {
if (error instanceof EmailAlreadyExistsError) {
res.status(409).json({ error: error.message });
}
}
}
}
src/
├── domain/
│ ├── entities/ (User, Order)
│ ├── value-objects/ (Email, Money, UserId)
│ ├── services/ (PricingService)
│ ├── repositories/ (Interfaces only)
│ └── errors/
├── application/
│ ├── use-cases/ (CreateUser, UpdateOrder)
│ ├── services/ (NotificationService)
│ ├── ports/ (EmailPort, PaymentPort)
│ └── dto/
├── infrastructure/
│ ├── repositories/ (PostgreSQL, MongoDB implementations)
│ ├── adapters/ (SendGrid, Stripe)
│ ├── orm/
│ └── config/
├── presentation/
│ ├── http/ (Controllers, Routes, Middleware)
│ ├── graphql/ (Resolvers)
│ └── cli/ (Commands)
├── shared/ (Utilities, Kernel helpers)
└── container/ (Dependency Injection setup)
// src/container/container.ts
import { Container } from 'inversify';
const container = new Container();
// Bind implementations to interfaces
container.bind<UserRepository>(TYPES.UserRepository)
.to(PostgreSQLUserRepository)
.inSingletonScope();
container.bind<CreateUserUseCase>(TYPES.CreateUserUseCase)
.to(CreateUserUseCase)
.inTransientScope();
container.bind<UserController>(TYPES.UserController)
.to(UserController)
.inTransientScope();
export { container };
Each layer has one reason to change:
Add features by creating new use cases, not modifying existing:
export class UpdateUserUseCase { /* ... */ }
Repository implementations are fully interchangeable:
const repo: UserRepository = new PostgreSQLUserRepository(db);
const repo: UserRepository = new MongoUserRepository(client);
// Both satisfy the contract
Use focused interfaces, not fat ones:
// Good: Segregated
interface UserCreator { create(data): User; }
interface UserDeleter { delete(id): void; }
// Bad: Fat interface
interface UserService {
create(): User;
update(): User;
delete(): void;
sendEmail(): void;
generateReport(): Report;
}
Depend on abstractions, not implementations:
// Application defines the port
export interface EmailPort {
send(to: string, subject: string, body: string): Promise<void>;
}
// Infrastructure implements
export class SendGridAdapter implements EmailPort {
async send(to: string, subject: string, body: string): Promise<void> {
await this.sendgrid.send({ to, subject, text: body });
}
}
// Use cases depend on port
export class CreateUserUseCase {
constructor(private readonly emailPort: EmailPort) {}
}
// Unit: Domain logic without infrastructure
describe('User', () => {
it('should change password', () => {
const hasher = new BCryptHasher();
const user = new User(UserId.generate(), hasher.hash('oldpass'));
user.changePassword(Password.create('newpass'), hasher);
expect(user.validatePassword(Password.create('newpass'), hasher)).toBe(true);
});
});
// Integration: Infrastructure with real DB
describe('PostgreSQLUserRepository', () => {
it('should save and retrieve user', async () => {
const repo = new PostgreSQLUserRepository(testDb);
const user = createTestUser();
await repo.save(user);
const retrieved = await repo.findById(user.id);
expect(retrieved).not.toBeNull();
});
});
// E2E: Full stack via HTTP
describe('User API', () => {
it('should create user via POST', async () => {
const response = await request(app).post('/api/users').send({
email: '[email protected]',
password: 'secure123'
});
expect(response.status).toBe(201);
});
});
// Bad: Business logic in controller
async create(req, res) {
if (await this.db.query('SELECT * FROM users WHERE email = $1', [req.body.email])) {
return res.status(409).json({ error: 'Email exists' });
}
}
// Good: Delegate to use case
async create(req, res) {
const result = await this.createUserUseCase.execute(req.body);
res.status(201).json(result);
}
// Bad: Infrastructure leak in entity
class User {
async save() {
await prisma.user.create({ data: this });
}
}
// Good: Repository handles persistence
class User { /* pure domain */ }
class UserRepository {
async save(user: User) { await prisma.user.create(...); }
}
// Bad: Entity is just data
class User {
id: string;
password: string;
}
class UserService {
changePassword(user: User, pwd: string) {
user.password = hash(pwd); // Logic outside entity
}
}
// Good: Rich domain model
class User {
changePassword(newPassword: Password, hasher: PasswordHasher): void {
if (!newPassword.isStrong()) throw new WeakPasswordError();
this.passwordHash = hasher.hash(newPassword);
}
}
development
Enhanced plan-authoring skill with Pre-Writing context gathering, task metadata, non-TDD templates, Red Flags, telemetry, and an automated plan linter. Use when you have a spec or requirements for a multi-step task, before touching code.
tools
Documentation intelligence engine with graph-based API docs, algorithm library, and drift detection
tools
Ultraplan cloud planning — kick off a plan in the cloud from your terminal, review and revise in the browser, then execute remotely or send back to CLI
tools
--- name: mcp description: Configure MCP servers for Claude Code — stdio vs HTTP, authentication, Tools/Resources/Prompts distinction, channels (CI webhook, mobile relay, Discord bridge, fakechat), and cost of always-loaded tools. Use this skill whenever adding an MCP server, debugging connection issues, choosing between MCP Tools vs Prompts vs Resources, installing channel servers, or managing .mcp.json. Triggers on: "MCP server", "mcp config", "add Obsidian MCP", "install context7", "channels"