Agent skill
domain-expert
Apply domain-driven design principles for business logic, entities, events and aggregate boundaries. Use when modeling domain concepts, implementing business rules, or defining clear separation between domain and infrastructure layers.
Install this agent skill to your Project
npx add-skill https://github.com/d-oit/do-novelist-ai/tree/main/.opencode/skill/domain-expert
SKILL.md
Domain Expert
Enforce domain-driven design (DDD) principles and ensure clean separation between domain logic, application logic, and infrastructure.
Quick Reference
Core Concepts:
- Bounded Contexts - Feature module isolation
- Domain Layers - Pure business logic separation
- Entity Modeling - Rich domain objects
- Value Objects - Immutable, equality-by-value
- Aggregates - Consistency boundaries
- Domain Events - Event-driven design
- Repository Pattern - Interface-based persistence
When to Use
- Modeling new domain concepts (entities, value objects, aggregates)
- Implementing business rules and invariants
- Designing domain events and event handlers
- Defining aggregate boundaries and consistency boundaries
- Ensuring domain layer remains pure and independent
- Writing feature modules in
src/features/
Core DDD Principles
Bounded Contexts
Each feature module represents a bounded context with its own domain model:
src/features/
├── characters/ # Character management bounded context
│ ├── types/ # Domain entities & value objects
│ ├── services/ # Domain services
│ └── components/ # UI components
├── projects/ # Project management bounded context
└── world-building/ # World management bounded context
Domain Layers
1. Domain Layer (Pure business logic)
- Domain entities and value objects
- Domain services
- Domain events
- Repository interfaces (only interfaces)
- Business rules and invariants
2. Application Layer (Use cases)
- Application services (orchestrate domain)
- DTOs for API boundaries
- Command/query handlers (CQRS pattern)
- Event handlers
3. Infrastructure Layer (Technical details)
- Repository implementations
- External service integrations
- Database operations
- File I/O
Domain Modeling
Entities:
- Have identity (ID)
- Contain business logic
- Enforce invariants
- Mutable state
export class Character {
constructor(
public id: string,
public name: string,
public attributes: CharacterAttributes,
) {}
addAttribute(key: string, value: string): void {
this.attributes[key] = value;
}
removeAttribute(key: string): void {
if (this.attributes[key]) {
delete this.attributes[key];
}
}
}
Value Objects:
- No identity (equality by value)
- Immutable
- Replaceable
- Validate on creation
export class CharacterAttributes {
constructor(
public readonly hairColor: string,
public readonly eyeColor: string,
public readonly height: number,
) {
if (height < 0 || height > 300) {
throw new Error('Invalid height');
}
}
equals(other: CharacterAttributes): boolean {
return (
this.hairColor === other.hairColor &&
this.eyeColor === other.eyeColor &&
this.height === other.height
);
}
}
Aggregate Design
Aggregate Rules:
- One aggregate root per consistency boundary
- All invariants enforced by aggregate root
- Aggregates are loaded and saved atomically
- External references only by ID
export class ProjectAggregate {
constructor(
public readonly project: Project,
private chapters: Chapter[],
private characters: Map<string, Character>,
) {}
addChapter(chapter: Chapter): void {
if (this.project.chapterCount >= this.project.maxChapters) {
throw new Error('Maximum chapters reached');
}
this.chapters.push(chapter);
this.domainEvents.push(new ChapterAddedEvent(this.project.id, chapter.id));
}
removeChapter(chapterId: string): void {
this.chapters = this.chapters.filter(c => c.id !== chapterId);
this.domainEvents.push(new ChapterRemovedEvent(this.project.id, chapterId));
}
private domainEvents: DomainEvent[] = [];
}
Domain Events
Event Design:
- Past tense (ChapterAdded, ProjectCreated)
- Immutable
- Carry minimal context
- No side effects (pure event data)
export class ChapterAddedEvent implements DomainEvent {
readonly eventType = 'ChapterAdded';
constructor(
public readonly projectId: string,
public readonly chapterId: string,
public readonly timestamp: Date = new Date(),
) {}
}
// Event handler (in application layer)
export class ChapterAddedHandler {
async handle(event: ChapterAddedEvent): Promise<void> {
await analyticsService.trackChapterCreated(event.chapterId);
}
}
Business Rules Implementation
Rule Enforcement
Invariants (Rules that must always hold):
- Domain entities enforce invariants
- Fail fast with clear errors
- Never allow invalid state
export class Chapter {
private wordCount: number;
setContent(text: string): void {
const words = text.split(/\s+/).length;
if (words < 100) {
throw new ValidationError('Chapter must have at least 100 words');
}
if (words > 10000) {
throw new ValidationError('Chapter cannot exceed 10,000 words');
}
this.wordCount = words;
}
}
Specification Pattern
Use specifications for reusable business rules:
export interface Specification<T> {
isSatisfiedBy(candidate: T): boolean;
}
export class ValidChapterSpecification implements Specification<Chapter> {
isSatisfiedBy(chapter: Chapter): boolean {
return chapter.wordCount >= 100 && chapter.wordCount <= 10000;
}
}
// Usage
if (!validChapterSpec.isSatisfiedBy(chapter)) {
throw new ValidationError('Invalid chapter');
}
Repository Pattern
Repository Interfaces (Domain Layer)
Define interfaces in domain, implement in infrastructure:
// Domain layer
export interface ChapterRepository {
findById(id: string): Promise<Chapter | null>;
save(chapter: Chapter): Promise<void>;
delete(id: string): Promise<void>;
findByProjectId(projectId: string): Promise<Chapter[]>;
}
Repository Implementation (Infrastructure Layer)
// Infrastructure layer
export class TursoChapterRepository implements ChapterRepository {
constructor(private db: LibSQLDatabase) {}
async findById(id: string): Promise<Chapter | null> {
const result = await this.db.execute(
'SELECT * FROM chapters WHERE id = ?',
[id],
);
return result.rows[0] ? Chapter.fromRow(result.rows[0]) : null;
}
async save(chapter: Chapter): Promise<void> {
await this.db.execute(
'INSERT INTO chapters (id, project_id, title, content) VALUES (?, ?, ?, ?)',
[chapter.id, chapter.projectId, chapter.title, chapter.content],
);
}
}
Common Patterns
Factory Pattern
Create complex aggregates with validation:
export class ProjectFactory {
static create(
userId: string,
title: string,
config: ProjectConfig,
): ProjectAggregate {
if (!title || title.trim().length < 3) {
throw new ValidationError('Title must be at least 3 characters');
}
const project = new Project(generateId(), userId, title);
const aggregate = new ProjectAggregate(project, [], new Map());
aggregate.applyConfig(config);
return aggregate;
}
}
Domain Service
Business logic that doesn't naturally belong to any entity:
export class ProjectPricingService {
calculatePrice(project: Project, usage: UsageMetrics): Price {
const basePrice = project.pricingTier.basePrice;
const wordCountBonus = (usage.totalWords / 1000) * 0.01;
const storagePenalty = usage.storageUsageGB * 0.5;
return {
base: basePrice,
adjustments: wordCountBonus + storagePenalty,
total: basePrice + wordCountBonus + storagePenalty,
};
}
}
Anti-Patterns to Avoid
❌ Anemic Domain Model
Don't make entities data-only objects:
// BAD - Anemic
class Chapter {
id: string;
title: string;
content: string;
}
// GOOD - Rich domain model
class Chapter {
constructor(
private id: string,
private title: string,
private content: string,
) {}
get title(): string {
return this.title;
}
setTitle(title: string): void {
if (title.length > 100) {
throw new Error('Title too long');
}
this.title = title;
}
}
❌ God Aggregates
Don't create aggregates that contain unrelated concepts:
// BAD - Too large
class ProjectAggregate {
project: Project;
chapters: Chapter[];
characters: Character[];
locations: Location[];
settings: Settings;
analytics: Analytics[];
// ...everything else
}
// GOOD - Bounded aggregates
class ProjectAggregate {
project: Project;
chapters: Chapter[];
}
class CharacterAggregate {
character: Character;
attributes: CharacterAttributes[];
}
❌ Leaking Infrastructure
Don't import infrastructure dependencies in domain:
// BAD - Database in domain
class Chapter {
async saveToDatabase(db: LibSQLDatabase): Promise<void> { ... }
}
// GOOD - Repository interface in domain
class Chapter {
// Pure domain logic only
}
// Infrastructure handles persistence
await chapterRepository.save(chapter);
Testing Domain Logic
Unit Tests for Domain Entities
describe('Chapter', () => {
describe('setContent', () => {
it('should throw error for too few words', () => {
const chapter = new Chapter('id', 'projectId', 'Title');
expect(() => chapter.setContent('short')).toThrow(ValidationError);
});
it('should throw error for too many words', () => {
const chapter = new Chapter('id', 'projectId', 'Title');
expect(() => chapter.setContent(longText)).toThrow(ValidationError);
});
it('should set content for valid word count', () => {
const chapter = new Chapter('id', 'projectId', 'Title');
chapter.setContent(validContent);
expect(chapter.wordCount).toBe(validWordCount);
});
});
});
Feature Module Structure
Follow feature-based architecture in src/features/:
src/features/feature-name/
├── types/ # Domain entities, value objects, DTOs
├── services/ # Domain services, application services
├── components/ # React components (UI layer)
├── hooks/ # Custom React hooks
├── index.ts # Public exports
└── [feature-name].test.ts # Feature tests
Best Practices Summary
DO:
✓ Model domain concepts as rich entities ✓ Use value objects for immutable concepts ✓ Define aggregate boundaries clearly ✓ Enforce invariants in domain layer ✓ Use repository interfaces for persistence ✓ Emit domain events for state changes ✓ Keep domain layer pure (no infrastructure)
DON'T:
✗ Create anemic domain models ✗ Put business logic in components ✗ Leak infrastructure into domain ✗ Create god aggregates ✗ Mix domain and application logic ✗ Use concrete database types in domain ✗ Allow invalid state in entities
Quick Reference: File Locations
- Domain types:
src/features/*/types/ - Domain services:
src/features/*/services/ - Repository interfaces:
src/features/*/types/orsrc/lib/*/repositories/ - Repository implementations:
src/lib/database/repositories/
Apply domain-driven design to keep business logic clean, testable, and independent of infrastructure.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
novel-development
Work on novel-specific features including plot engines, character development, world-building, timeline management, and GOAP-based story generation. Use when implementing narrative systems, character arcs, or story planning tools.
tech-stack-specialist
Manage framework usage, dependencies, build configuration, and environment setup. Use when adding new dependencies, updating packages, configuring build tools, or setting up development environment.
performance-engineer
Optimize application performance including build times, runtime speed, bundle size and resource usage. Use when addressing performance issues, implementing caching strategies, or optimizing rendering.
e2e-test-optimizer
Optimize Playwright E2E tests by removing anti-patterns, implementing smart waits, enabling test sharding, and improving reliability.
qa-engineer
Define comprehensive testing strategies, write tests with proper naming conventions, organize tests by type, and implement mocking strategies. Use when creating tests, refactoring test suites, or improving test coverage.
writing-assistant
Work on writing assistance features including real-time style analysis, grammar checking, writing goals tracking, inline suggestions, and writing analytics. Use when implementing linguistic analysis, productivity tracking, or writing quality tools.
Didn't find tool you were looking for?