.claude/plugins/jira-orchestrator/skills/clean-architecture/SKILL.md
Clean Architecture and SOLID principles implementation including dependency injection, layer separation, domain-driven design, hexagonal architecture, and code quality patterns
npx skillsauth add markus41/demo-0.2 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.
Activate this skill when:
Dependencies must point inward. Inner layers must not know about outer layers.
┌─────────────────────────────────────────────────────────────┐
│ EXTERNAL LAYER │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ INFRASTRUCTURE LAYER │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ APPLICATION LAYER │ │ │
│ │ │ ┌─────────────────────────────────────────────────┐ │ │ │
│ │ │ │ DOMAIN LAYER │ │ │ │
│ │ │ │ (Entities, Value Objects) │ │ │ │
│ │ │ └─────────────────────────────────────────────────┘ │ │ │
│ │ │ (Use Cases, Application Services) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ (Repositories, External Services, ORM) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ (Web Framework, Database, UI, External APIs) │
└─────────────────────────────────────────────────────────────┘
Dependencies point INWARD →
The heart of the application. Contains:
// src/domain/entities/user.entity.ts
export class User {
constructor(
public readonly id: UserId,
public email: Email,
public name: UserName,
private passwordHash: PasswordHash,
public readonly createdAt: Date
) {}
changePassword(newPassword: Password, hasher: PasswordHasher): void {
this.passwordHash = hasher.hash(newPassword);
}
validatePassword(password: Password, hasher: PasswordHasher): boolean {
return hasher.verify(password, this.passwordHash);
}
}
// src/domain/value-objects/email.vo.ts
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);
}
toString(): string {
return this.value;
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
// src/domain/repository-interfaces/user.repository.ts
export interface UserRepository {
findById(id: UserId): Promise<User | null>;
findByEmail(email: Email): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: UserId): Promise<void>;
}
Orchestrates domain objects to perform use cases:
// src/application/use-cases/create-user.use-case.ts
export interface CreateUserInput {
email: string;
name: string;
password: string;
}
export interface CreateUserOutput {
id: string;
email: string;
name: string;
createdAt: Date;
}
export class CreateUserUseCase {
constructor(
private readonly userRepository: UserRepository,
private readonly passwordHasher: PasswordHasher,
private readonly eventEmitter: DomainEventEmitter
) {}
async execute(input: CreateUserInput): Promise<CreateUserOutput> {
// Validate email uniqueness
const existingUser = await this.userRepository.findByEmail(
Email.create(input.email)
);
if (existingUser) {
throw new EmailAlreadyExistsError(input.email);
}
// Create domain entity
const user = new User(
UserId.generate(),
Email.create(input.email),
UserName.create(input.name),
this.passwordHasher.hash(Password.create(input.password)),
new Date()
);
// Persist
await this.userRepository.save(user);
// Emit domain event
this.eventEmitter.emit(new UserCreatedEvent(user));
// Return DTO
return {
id: user.id.toString(),
email: user.email.toString(),
name: user.name.toString(),
createdAt: user.createdAt
};
}
}
Implements interfaces defined in inner layers:
// src/infrastructure/repositories/postgresql-user.repository.ts
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 findByEmail(email: Email): Promise<User | null> {
const row = await this.db.query(
'SELECT * FROM users WHERE email = $1',
[email.toString()]
);
return row ? this.toDomain(row) : null;
}
async save(user: User): Promise<void> {
await this.db.query(
`INSERT INTO users (id, email, name, password_hash, created_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE SET
email = $2, name = $3, password_hash = $4`,
[user.id.toString(), user.email.toString(), user.name.toString(),
user.passwordHash, user.createdAt]
);
}
private toDomain(row: UserRow): User {
return new User(
UserId.fromString(row.id),
Email.create(row.email),
UserName.create(row.name),
PasswordHash.fromString(row.password_hash),
row.created_at
);
}
}
Entry points to the application:
// src/presentation/http/controllers/user.controller.ts
export class UserController {
constructor(
private readonly createUserUseCase: CreateUserUseCase,
private readonly getUserUseCase: GetUserUseCase
) {}
async create(req: Request, res: Response): Promise<void> {
try {
const result = await this.createUserUseCase.execute({
email: req.body.email,
name: req.body.name,
password: req.body.password
});
res.status(201).json(result);
} catch (error) {
if (error instanceof EmailAlreadyExistsError) {
res.status(409).json({ error: error.message });
} else if (error instanceof ValidationError) {
res.status(400).json({ error: error.message });
} else {
throw error;
}
}
}
}
src/
├── domain/ # Domain Layer (Core)
│ ├── entities/
│ │ ├── user.entity.ts
│ │ └── order.entity.ts
│ ├── value-objects/
│ │ ├── email.vo.ts
│ │ ├── money.vo.ts
│ │ └── user-id.vo.ts
│ ├── events/
│ │ ├── user-created.event.ts
│ │ └── order-placed.event.ts
│ ├── services/
│ │ └── pricing.domain-service.ts
│ ├── repositories/ # Interfaces only!
│ │ ├── user.repository.ts
│ │ └── order.repository.ts
│ └── errors/
│ ├── domain-error.ts
│ └── validation-error.ts
│
├── application/ # Application Layer
│ ├── use-cases/
│ │ ├── user/
│ │ │ ├── create-user.use-case.ts
│ │ │ ├── get-user.use-case.ts
│ │ │ └── update-user.use-case.ts
│ │ └── order/
│ │ ├── create-order.use-case.ts
│ │ └── cancel-order.use-case.ts
│ ├── services/
│ │ └── notification.service.ts
│ ├── ports/ # Secondary ports
│ │ ├── email.port.ts
│ │ └── payment.port.ts
│ └── dto/
│ ├── user.dto.ts
│ └── order.dto.ts
│
├── infrastructure/ # Infrastructure Layer
│ ├── repositories/
│ │ ├── postgresql-user.repository.ts
│ │ └── postgresql-order.repository.ts
│ ├── adapters/
│ │ ├── sendgrid-email.adapter.ts
│ │ └── stripe-payment.adapter.ts
│ ├── orm/
│ │ ├── prisma/
│ │ │ └── schema.prisma
│ │ └── migrations/
│ ├── messaging/
│ │ ├── rabbitmq-publisher.ts
│ │ └── rabbitmq-consumer.ts
│ └── config/
│ └── database.config.ts
│
├── presentation/ # Presentation Layer
│ ├── http/
│ │ ├── controllers/
│ │ │ ├── user.controller.ts
│ │ │ └── order.controller.ts
│ │ ├── middleware/
│ │ │ ├── auth.middleware.ts
│ │ │ └── error-handler.middleware.ts
│ │ ├── routes/
│ │ │ └── index.ts
│ │ └── validators/
│ │ └── user.validator.ts
│ ├── graphql/
│ │ ├── resolvers/
│ │ └── schema/
│ └── cli/
│ └── commands/
│
├── shared/ # Cross-cutting concerns
│ ├── kernel/
│ │ ├── result.ts
│ │ └── either.ts
│ └── utils/
│ └── date.utils.ts
│
└── container/ # Dependency Injection
├── container.ts
└── providers/
├── user.provider.ts
└── order.provider.ts
// src/container/container.ts
import { Container } from 'inversify';
import { TYPES } from './types';
// Domain
import { UserRepository } from '@/domain/repositories/user.repository';
// Application
import { CreateUserUseCase } from '@/application/use-cases/user/create-user.use-case';
// Infrastructure
import { PostgreSQLUserRepository } from '@/infrastructure/repositories/postgresql-user.repository';
// Presentation
import { UserController } from '@/presentation/http/controllers/user.controller';
const container = new Container();
// Bind repositories (interface → implementation)
container.bind<UserRepository>(TYPES.UserRepository)
.to(PostgreSQLUserRepository)
.inSingletonScope();
// Bind use cases
container.bind<CreateUserUseCase>(TYPES.CreateUserUseCase)
.to(CreateUserUseCase)
.inTransientScope();
// Bind controllers
container.bind<UserController>(TYPES.UserController)
.to(UserController)
.inTransientScope();
export { container };
// src/container/types.ts
export const TYPES = {
// Repositories
UserRepository: Symbol.for('UserRepository'),
OrderRepository: Symbol.for('OrderRepository'),
// Use Cases
CreateUserUseCase: Symbol.for('CreateUserUseCase'),
GetUserUseCase: Symbol.for('GetUserUseCase'),
// Adapters
EmailAdapter: Symbol.for('EmailAdapter'),
PaymentAdapter: Symbol.for('PaymentAdapter'),
// Controllers
UserController: Symbol.for('UserController'),
OrderController: Symbol.for('OrderController'),
};
Each layer has one responsibility:
Add features by adding new use cases, not modifying existing ones:
// Add new feature: UpdateUserUseCase
// Don't modify: CreateUserUseCase
export class UpdateUserUseCase { /* ... */ }
Repository implementations are fully substitutable:
// Both work with UserRepository interface
const postgresRepo: UserRepository = new PostgreSQLUserRepository(db);
const mongoRepo: UserRepository = new MongoUserRepository(client);
const memoryRepo: UserRepository = new InMemoryUserRepository();
Small, focused interfaces:
// BAD: Fat interface
interface UserService {
create(data): User;
update(id, data): User;
delete(id): void;
sendEmail(id): void;
generateReport(id): Report;
exportToCSV(id): string;
}
// GOOD: Segregated
interface UserCreator { create(data): User; }
interface UserUpdater { update(id, data): User; }
interface UserDeleter { delete(id): void; }
interface UserEmailer { sendEmail(id): void; }
All dependencies point to abstractions:
// Application layer defines the port (interface)
export interface EmailPort {
send(to: string, subject: string, body: string): Promise<void>;
}
// Infrastructure implements the adapter
export class SendGridEmailAdapter implements EmailPort {
async send(to: string, subject: string, body: string): Promise<void> {
await this.sendgrid.send({ to, subject, text: body });
}
}
// Use case depends on abstraction, not implementation
export class CreateUserUseCase {
constructor(private readonly emailPort: EmailPort) {}
}
// Test domain logic without infrastructure
describe('User', () => {
it('should validate password correctly', () => {
const hasher = new BCryptHasher();
const password = Password.create('SecureP@ss1');
const user = new User(
UserId.generate(),
Email.create('[email protected]'),
UserName.create('Test User'),
hasher.hash(password),
new Date()
);
expect(user.validatePassword(password, hasher)).toBe(true);
expect(user.validatePassword(Password.create('wrong'), hasher)).toBe(false);
});
});
// Test use cases with mock repositories
describe('CreateUserUseCase', () => {
it('should create user successfully', async () => {
const mockRepo = {
findByEmail: jest.fn().mockResolvedValue(null),
save: jest.fn().mockResolvedValue(undefined)
};
const mockHasher = { hash: jest.fn().mockReturnValue('hashed') };
const mockEmitter = { emit: jest.fn() };
const useCase = new CreateUserUseCase(mockRepo, mockHasher, mockEmitter);
const result = await useCase.execute({
email: '[email protected]',
name: 'Test User',
password: 'password123'
});
expect(result.email).toBe('[email protected]');
expect(mockRepo.save).toHaveBeenCalled();
expect(mockEmitter.emit).toHaveBeenCalled();
});
});
describe('PostgreSQLUserRepository', () => {
let repository: PostgreSQLUserRepository;
let db: Database;
beforeAll(async () => {
db = await createTestDatabase();
repository = new PostgreSQLUserRepository(db);
});
afterAll(async () => {
await db.close();
});
it('should save and retrieve user', async () => {
const user = createTestUser();
await repository.save(user);
const retrieved = await repository.findById(user.id);
expect(retrieved).not.toBeNull();
expect(retrieved.email.equals(user.email)).toBe(true);
});
});
describe('User API', () => {
it('should create user via HTTP', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: '[email protected]',
name: 'Test User',
password: 'password123'
});
expect(response.status).toBe(201);
expect(response.body.email).toBe('[email protected]');
});
});
// BAD
class UserController {
async create(req, res) {
// Business logic in controller!
if (await this.db.query('SELECT * FROM users WHERE email = $1', [req.body.email])) {
return res.status(409).json({ error: 'Email exists' });
}
const hash = await bcrypt.hash(req.body.password, 10);
await this.db.query('INSERT INTO users...');
}
}
// GOOD
class UserController {
async create(req, res) {
const result = await this.createUserUseCase.execute(req.body);
res.status(201).json(result);
}
}
// BAD
class User {
async save() {
await prisma.user.create({ data: this }); // Infrastructure leak!
}
}
// GOOD
class User {
// Pure domain logic, no infrastructure
}
// Repository handles persistence
class UserRepository {
async save(user: User) {
await prisma.user.create({ data: user.toDTO() });
}
}
// BAD - No behavior, just data
class User {
id: string;
email: string;
password: string;
}
class UserService {
changePassword(user: User, newPassword: string) {
user.password = hash(newPassword); // Logic outside entity
}
}
// GOOD - Rich domain model
class User {
constructor(private readonly id: UserId, private passwordHash: PasswordHash) {}
changePassword(newPassword: Password, hasher: PasswordHasher): void {
if (!newPassword.isStrong()) {
throw new WeakPasswordError();
}
this.passwordHash = hasher.hash(newPassword);
}
}
development
This skill should be used when the user asks to "triage issue", "classify ticket", "route jira", "analyze priority", "categorize issue", "determine complexity", "route to agents", or needs guidance on classifying, prioritizing, and routing Jira issues to appropriate agents and workflows.
data-ai
Enriches Jira tasks with comprehensive context, requirements analysis, and technical details through intelligent extraction, dependency mapping, and historical analysis
development
Comprehensive knowledge for creating, managing, and merging pull requests with Jira integration, following best practices for code review, deployment, and team collaboration
tools
This skill should be used when the user asks to "orchestrate jira", "work on issue", "complete jira ticket", "development workflow", "jira automation", "issue lifecycle", "work on story", "fix bug ticket", or needs guidance on coordinating development work through Jira with multi-agent orchestration patterns.