
Tactical Ddd
- 77 installs
- 5k repo stars
- Updated August 4, 2026
- tech-leads-club/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
tactical-ddd is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- tactical-ddd
- AI & Agent Building
- AI-coding skill
Tactical Ddd by the numbers
- 77 all-time installs (skills.sh)
- +7 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #5,358 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tech-leads-club/agent-skills --skill tactical-dddAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 77 |
|---|---|
| repo stars | ★ 5k |
| Last updated | August 4, 2026 |
| Repository | tech-leads-club/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Tactical DDD — Rich Domain Modeling
Workflow
Determine the user's intent first:
| Intent | Phases to run |
|---|---|
| "validate / review / check / is this correct?" | Phase 1 + 2 only → report findings, ask before refactoring |
| "fix / refactor / improve / clean up" | Phase 1 + 2 + 3 |
| "how should I design / model this?" | Load reference.md directly |
Phase 1 — Detect
Load detection.md and scan the target code for anemia signals. Produce a severity score and list of affected classes.
Phase 2 — Assess
For each affected class, determine the correct building block:
| Has unique identity tracked over time? | Has invariants tying multiple objects? | → Building Block |
|---|---|---|
| Yes | — | Entity |
| No | — | Value Object |
| Yes (root) + children with shared invariants | Yes | Aggregate |
| Operation spans multiple Aggregates/doesn't belong to any | — | Domain Service |
Prefer Value Objects over Entities. Prefer small Aggregates over large ones.
If intent was validate/review: stop here. Report findings using the output format below. Ask "Would you like me to apply these fixes?" before proceeding.
Phase 3 — Refactor
Load refactoring.md for step-by-step moves. Apply in this order: 1. Replace setter chains with a single expressive method 2. Move service logic into the Aggregate that owns it 3. Add business guards at the top of each method 4. Publish a Domain Event after each successful state change 5. Replace primitive types with Value Objects
For deep pattern questions (boundary design, event modeling, service vs. entity decision), load reference.md.
---
Quick Anemia Signals (scan first)
public setX() / public setY() → behaviour should be encapsulated
service.doX(entity, ...) → logic likely belongs in entity
entity.setA(); entity.setB(); ... → setter chain = missing intent method
no domain methods beyond getters → pure data bag---
Golden Rules
1. Behaviour with data — Objects own both state and the operations that change it 2. Ubiquitous Language — Method names come from the domain, not CRUD (commitTo, not setStatus) 3. Small Aggregates — Root + Value Objects by default; add child Entities only for true invariants 4. One transaction = one Aggregate — Cross-Aggregate rules use eventual consistency via Domain Events 5. Reference by ID — Never hold object references to other Aggregates 6. Value Objects first — Use Entities only when individual identity is essential 7. Domain Services sparingly — Excessive services → anemic model 8. Protect invariants — The Aggregate is the last line of defence; never trust the caller
---
Output Format
When reviewing code, report:
## Anemia Diagnosis: <ClassName>
Severity: [None | Mild | Moderate | Severe]
Issues:
- <description of problem>
Recommended refactoring:
- <specific move from refactoring.md>When refactoring, show a before/after diff for each class touched.
Anemia Detection
Step 1 — Code Signals to Scan For
Search for these patterns. Each match is a potential anemia indicator:
| Signal | Pattern | Weight |
|---|---|---|
| Public setter | public set[A-Z] | +2 |
| Setter chain in caller | entity.setA(); entity.setB() | +3 |
| Logic in Application/Service layer that mutates entity | entity.setX(computedValue) inside service method | +3 |
| Class with only getters/setters, no domain methods | All methods match `get.*\ | set.*\ |
| Primitive obsession instead of Value Objects | string customerId, number amount on Entity | +1 |
| Coordinator pattern: service fetches + mutates | repo.find() then multiple entity.setX() | +2 |
| Missing guards | Methods with no precondition checks | +1 |
Step 2 — Severity Score
Sum the weights across all signals in the class:
| Score | Severity | Meaning |
|---|---|---|
| 0 | None | Well-modelled |
| 1–3 | Mild | Minor improvements; priorities elsewhere |
| 4–6 | Moderate | Refactor; business logic is leaking |
| 7+ | Severe | Full redesign; domain is just a DTO |
Step 3 — Class-Level Checklist
For each class under review:
- [ ] Does it have public setters for business-meaningful fields?
- [ ] Do callers set multiple fields to perform a single operation?
- [ ] Is the class used as a parameter bag by services that contain the real logic?
- [ ] Are there zero methods that express domain intent?
- [ ] Are IDs and measures stored as primitives instead of Value Objects?
- [ ] Are there no Domain Events published after state changes?
Common Anemia Patterns
The Coordinator Service
// ❌ Logic lives outside the entity
class OrderService {
confirm(orderId: string): void {
const order = this.repo.find(orderId);
order.setStatus('CONFIRMED'); // setter
order.setConfirmedAt(new Date()); // setter
order.setConfirmedBy(this.userId); // setter
this.repo.save(order);
}
}Signal: Three setters to express one business operation.
The Data Transfer Object Disguised as Entity
// ❌ Pure data bag
class Product {
getName(): string { return this.name; }
setName(v: string) { this.name = v; }
getPrice(): number { return this.price; }
setPrice(v: number) { this.price = v; }
getStock(): number { return this.stock; }
setStock(v: number) { this.stock = v; }
}Signal: Zero domain behaviour, all methods are accessors.
Primitive Obsession
// ❌ Primitives lose domain meaning and validation
class Order {
customerId: string; // Should be CustomerId VO
totalAmount: number; // Should be Money VO
currency: string; // Part of Money VO
}Refactoring Moves
Apply moves in order. Stop when the class passes all detection checks.
---
Move 1 — Replace setter chain with intent method
When: Multiple setters called together to perform one operation.
// Before ❌
order.setStatus('CONFIRMED');
order.setConfirmedAt(new Date());
order.setConfirmedBy(userId);
// After ✅
class Order {
confirm(confirmedBy: UserId): void {
if (this.status !== OrderStatus.PENDING) {
throw new Error('Only pending orders can be confirmed.');
}
this.status = OrderStatus.CONFIRMED;
this.confirmedAt = new Date();
this.confirmedBy = confirmedBy;
DomainEventPublisher.publish(new OrderConfirmed(this.orderId, confirmedBy));
}
}
order.confirm(userId);Rules:
- The method name must come from the Ubiquitous Language
- All business guards go at the top (fail fast)
- Publish a Domain Event after the state change succeeds
---
Move 2 — Pull service logic into the Aggregate
When: A service method fetches an Aggregate and then does business logic on it.
// Before ❌ — logic lives in service
class DiscountService {
apply(orderId: string, pct: number): void {
const order = this.repo.find(orderId);
if (pct < 0 || pct > 100) throw new Error('Invalid discount');
const discounted = order.getTotal() * (1 - pct / 100);
order.setTotal(discounted);
order.setDiscountApplied(true);
}
}
// After ✅ — logic inside Aggregate
class Order {
applyDiscount(discount: Discount): void {
if (this.discountApplied) throw new Error('Discount already applied.');
this.total = this.total.applyDiscount(discount);
this.discountApplied = true;
DomainEventPublisher.publish(new OrderDiscountApplied(this.orderId, discount));
}
}
// Service becomes a thin coordinator
class DiscountService {
apply(orderId: string, pct: number): void {
const order = this.repo.find(orderId);
order.applyDiscount(new Discount(pct)); // VO validates range
this.repo.save(order);
}
}---
Move 3 — Extract primitive to Value Object
When: Primitives carry domain meaning or validation rules.
// Before ❌
class Order {
customerId: string;
totalAmount: number;
currency: string;
}
// After ✅
class CustomerId {
constructor(private readonly value: string) {
if (!value) throw new Error('CustomerId cannot be empty');
}
equals(other: CustomerId): boolean { return this.value === other.value; }
toString(): string { return this.value; }
}
class Money {
constructor(readonly amount: number, readonly currency: string) {
if (amount < 0) throw new Error('Amount cannot be negative');
if (!currency) throw new Error('Currency required');
}
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error('Currency mismatch');
return new Money(this.amount + other.amount, this.currency);
}
applyDiscount(discount: Discount): Money {
return new Money(this.amount * (1 - discount.rate), this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
class Order {
constructor(readonly customerId: CustomerId, private total: Money) {}
}---
Move 4 — Add business guards
When: Methods mutate state without preconditions.
Every intent method should start with: 1. Pre-state validation (if (this.status !== X) throw) 2. Cross-field consistency checks 3. Then state mutation 4. Then Domain Event publication
cancel(reason: string): void {
// Guard 1: correct state
if (this.status === OrderStatus.DELIVERED) {
throw new Error('Cannot cancel a delivered order.');
}
// Guard 2: required data
if (!reason || reason.trim().length === 0) {
throw new Error('Cancellation reason is required.');
}
this.status = OrderStatus.CANCELLED;
this.cancellationReason = reason;
DomainEventPublisher.publish(new OrderCancelled(this.orderId, reason));
}---
Move 5 — Break Domain Service from Application Service
When: An Application Service contains business rules, not just coordination.
| Belongs in Domain Service | Belongs in Application Service |
|---|---|
| Business rules involving multiple Aggregates | Loading Aggregates from repositories |
| Domain calculations | Transaction management |
| Validation spanning Aggregates | Calling Domain Services |
| Stateless business operations | Mapping to/from DTOs |
// Domain Service — business logic
class AuthenticationService {
authenticate(tenantId: TenantId, username: string, password: string): UserDescriptor | null {
const tenant = this.tenantRepo.findById(tenantId);
if (!tenant?.isActive()) return null;
const user = this.userRepo.findByCredentials(tenantId, username, this.encrypt(password));
return user?.isEnabled() ? user.toDescriptor() : null;
}
}
// Application Service — coordinates
class UserAppService {
login(tenantId: string, username: string, password: string): UserDescriptorDto {
const descriptor = this.authService.authenticate(
new TenantId(tenantId), username, password
);
if (!descriptor) throw new UnauthorizedError();
return UserDescriptorMapper.toDto(descriptor);
}
}---
Domain Event Checklist (after each move)
- [ ] Named in past tense using Ubiquitous Language (
OrderConfirmed, notStatusChanged) - [ ] Published after successful state change, not before
- [ ] Contains only data the Aggregate already has (no extra repo calls)
- [ ] Immutable (all fields
readonly) - [ ] Carries
occurredOn: Dateand optionallyeventVersion
Tactical DDD Reference
Entity
Use when: Object needs individual identity tracked over time, even if attributes change.
class Entity<T> {
constructor(protected readonly id: T) {}
equals(other: Entity<T>): boolean {
if (!other || this.constructor !== other.constructor) return false;
return this.id === other.id; // identity only
}
}Rules:
- Identity is
readonly, set once in constructor, never changed - Setters are
private; public API uses expressive methods - Publishes Domain Events on significant state changes
- Validates invariants in constructor and in each mutating method
Checklist:
- [ ] Unique immutable identity?
- [ ] Equality by identity (not attributes)?
- [ ] Methods express Ubiquitous Language?
- [ ] Setters private/protected?
- [ ] Domain Events on significant changes?
- [ ] Invariants validated on construction and mutation?
---
Value Object
Use when: Concept is described by its attributes, has no individual identity.
class Money {
constructor(readonly amount: number, readonly currency: string) {
if (amount < 0) throw new Error('Amount cannot be negative');
}
add(other: Money): Money { // side-effect-free
if (this.currency !== other.currency) throw new Error('Currency mismatch');
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean { // value equality
return this.amount === other.amount && this.currency === other.currency;
}
}
// Mutation = replacement
let price = new Money(100, 'USD');
price = price.add(new Money(20, 'USD')); // new object, not mutationRules:
- All fields
readonly— never mutate after construction - Side-effect-free methods return new instances
- Equality compares all attributes
- Validates on construction
Common VOs: UserId, OrderId, Money, Address, EmailAddress, DateRange, FullName
Checklist:
- [ ] Fully immutable (all fields
readonly)? - [ ] Equality by value?
- [ ] Methods return new instances?
- [ ] Forms a conceptual whole?
- [ ] Validated on construction?
---
Aggregate
Use when: A group of Entities + VOs must be consistent as a unit.
Four rules:
| Rule | Description |
|---|---|
| True invariants only | Only group objects when there's a business rule requiring them to be consistent in the same transaction |
| Keep it small | Root + VOs by default. Add child Entities only for true invariants |
| Reference by ID | sprintId: SprintId not sprint: Sprint |
| One Aggregate per transaction | Cross-Aggregate coordination uses Domain Events |
// ✅ Small Aggregate with true invariant
class BacklogItem {
private tasks: Task[] = [];
private status: BacklogItemStatus;
private productId: ProductId; // reference by ID — not Product object
private sprintId: SprintId | null;
estimateTaskHours(taskId: TaskId, hours: number): void {
this.findTask(taskId).estimateHoursRemaining(hours);
if (this.allTasksCompleted()) {
this.status = BacklogItemStatus.DONE;
}
// Invariant: task hours affect item status — true invariant justifying grouping
}
commitTo(sprint: Sprint): void {
if (!this.isScheduledForRelease()) throw new Error('Must be scheduled.');
if (this.isCommittedToSprint() && sprint.sprintId !== this.sprintId) {
this.uncommitFromSprint();
}
this.sprintId = sprint.sprintId;
this.status = BacklogItemStatus.COMMITTED;
DomainEventPublisher.publish(new BacklogItemCommitted(this.backlogItemId, sprint.sprintId));
}
}When to break the one-transaction rule (rare):
- UI batch creation with no invariants between items
- No messaging infrastructure available
- Explicit global transaction policy (document the reason in code)
Checklist:
- [ ] Clear Root Entity?
- [ ] True invariant justifies the grouping?
- [ ] Small enough (no large collections)?
- [ ] Other Aggregates referenced by ID only?
- [ ] One Aggregate per transaction?
- [ ] Eventual consistency via Domain Events for cross-Aggregate rules?
---
Domain Service
Use when: A business operation involves multiple Aggregates or doesn't naturally belong to any single one.
Warning: Overuse leads back to anemic model. Ask first: "Can this live in the Aggregate?"
| Criterion | Belongs in Entity/Aggregate | Belongs in Domain Service |
|---|---|---|
| Involves only one Aggregate's state | ✅ | — |
| Requires loading multiple Aggregates | — | ✅ |
| Calculation needs context from many objects | — | ✅ |
| Stateless business rule | ✅ (if single aggregate) | ✅ (if multi-aggregate) |
// ✅ Domain Service: authentication spans Tenant + User
class AuthenticationService {
authenticate(tenantId: TenantId, username: string, password: string): UserDescriptor | null {
const tenant = this.tenantRepo.findById(tenantId);
if (!tenant?.isActive()) return null;
const encrypted = this.encryptionService.encrypt(password);
const user = this.userRepo.findByCredentials(tenantId, username, encrypted);
return user?.isEnabled() ? user.toDescriptor() : null;
}
}Checklist:
- [ ] Operation doesn't belong to any single Entity or VO?
- [ ] Stateless?
- [ ] Expresses Ubiquitous Language?
- [ ] NOT being used to avoid behaviour in Entities?
- [ ] Business rules here, not in Application Service?
---
Domain Events
Naming: Past tense + Ubiquitous Language. OrderConfirmed, BacklogItemCommitted, UserRegistered.
interface DomainEvent {
readonly occurredOn: Date;
readonly eventVersion: number;
}
class OrderConfirmed implements DomainEvent {
readonly occurredOn = new Date();
readonly eventVersion = 1;
constructor(
readonly orderId: OrderId,
readonly confirmedBy: UserId,
) {}
}Publication pattern: 1. Complete state change 2. Publish event (state is already consistent) 3. Subscribers run in separate transactions for cross-Aggregate consistency
Checklist:
- [ ] Named in past tense?
- [ ] All fields
readonly? - [ ] Published after (not during) state change?
- [ ] Carries only data the Aggregate already owns?
- [ ] Cross-Aggregate handlers run in separate transactions?