| name | refactor-to-pattern |
| description | Refactor code to Clean Architecture and DDD patterns. Use when improving code to follow Clean Architecture principles, fixing anti-patterns, or applying DDD patterns. |
Refactor to Pattern: $0
Target: $1
Pattern options: anemic-to-rich, extract-value-object, extract-usecase, primitive-obsession, extract-aggregate, move-to-domain
Pattern: Anemic Domain Model → Rich Domain Model
Problem: Entity has only getters/setters with no behavior. Business logic lives in services/use cases instead of domain.
Step 1: Analyze Current Code
Read the target entity and identify:
- ❌ Public setters allowing uncontrolled state mutations
- ❌ No business methods (only getters/setters)
- ❌ Validation in use cases instead of entity
- ❌ Business logic scattered in services
Example of Anemic Model:
export class Order {
private _id: string;
private _status: OrderStatus;
private _total: number;
private _items: OrderItem[];
get status(): OrderStatus {
return this._status;
}
set status(value: OrderStatus) {
this._status = value;
}
get total(): number {
return this._total;
}
set total(value: number) {
this._total = value;
}
}
class OrderService {
async cancelOrder(orderId: string) {
const order = await this.repository.findById(orderId);
if (order.status === OrderStatus.Shipped) {
throw new Error('Cannot cancel shipped order');
}
order.status = OrderStatus.Cancelled;
await this.repository.save(order);
}
}
Step 2: Refactor to Rich Domain Model
Transform to:
export class Order extends AggregateRoot<OrderProps> {
private constructor(props: OrderProps, id?: UniqueEntityID) {
super(props, id);
}
public static create(props: OrderProps, id?: UniqueEntityID): Result<Order> {
const guardResult = Guard.againstNullOrUndefinedBulk([
{ argument: props.customerId, argumentName: 'customerId' },
{ argument: props.items, argumentName: 'items' },
]);
if (!guardResult.succeeded) {
return Result.fail<Order>(guardResult.message);
}
if (props.items.length === 0) {
return Result.fail<Order>('Order must contain at least one item');
}
const order = new Order(props, id);
order.addDomainEvent(new OrderCreatedEvent(order.id));
return Result.ok<Order>(order);
}
public cancel(): Result<void> {
if (this.props.status === OrderStatus.Shipped) {
return Result.fail('Cannot cancel order that has been shipped');
}
if (this.props.status === OrderStatus.Delivered) {
return Result.fail('Cannot cancel delivered order');
}
this.props.status = OrderStatus.Cancelled;
this.addDomainEvent(new OrderCancelledEvent(this.id));
return Result.ok();
}
public addItem(item: OrderItem): Result<void> {
if (this.props.items.length >= 100) {
return Result.fail('Order cannot exceed 100 items');
}
this.props.items.push(item);
this.recalculateTotal();
return Result.ok();
}
private recalculateTotal(): void {
this.props.total = this.props.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
get status(): OrderStatus {
return this.props.status;
}
get total(): number {
return this.props.total;
}
get items(): ReadonlyArray<OrderItem> {
return this.props.items;
}
}
class CancelOrderUseCase {
async execute(orderId: string): Promise<Result<void>> {
const orderResult = await this.repository.findById(orderId);
if (orderResult.isFailure) {
return Result.fail('Order not found');
}
const order = orderResult.getValue();
const cancelResult = order.cancel();
if (cancelResult.isFailure) {
return cancelResult;
}
await this.repository.save(order);
return Result.ok();
}
}
Step 3: Refactoring Checklist
Pattern: Primitive Obsession → Value Object
Problem: Using primitives (string, number) instead of meaningful domain concepts.
Step 1: Identify Primitive Obsession
Look for:
- ❌ Strings representing emails, phone numbers, money
- ❌ Numbers with business meaning (age, price, quantity)
- ❌ Multiple related primitives (amount + currency, street + city + zip)
- ❌ Validation scattered across codebase
Example:
interface UserProps {
email: string;
age: number;
balance: number;
phone: string;
}
if (!email.match(/^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/)) {
throw new Error('Invalid email');
}
Step 2: Extract Value Object
Create the Value Object:
interface EmailProps {
value: string;
}
export class Email extends ValueObject<EmailProps> {
private constructor(props: EmailProps) {
super(props);
}
public static create(email: string): Result<Email> {
if (!email) {
return Result.fail('Email is required');
}
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return Result.fail('Invalid email format');
}
return Result.ok(new Email({ value: email.toLowerCase() }));
}
get value(): string {
return this.props.value;
}
public getDomain(): string {
return this.props.value.split('@')[1];
}
}
interface MoneyProps {
amount: number;
currency: string;
}
export class Money extends ValueObject<MoneyProps> {
private constructor(props: MoneyProps) {
super(props);
}
public static create(amount: number, currency: string): Result<Money> {
if (amount < 0) {
return Result.fail('Amount cannot be negative');
}
const validCurrencies = ['USD', 'EUR', 'GBP'];
if (!validCurrencies.includes(currency)) {
return Result.fail(`Invalid currency: ${currency}`);
}
return Result.ok(new Money({ amount, currency }));
}
get amount(): number {
return this.props.amount;
}
get currency(): string {
return this.props.currency;
}
public add(other: Money): Result<Money> {
if (this.currency !== other.currency) {
return Result.fail('Cannot add different currencies');
}
return Money.create(this.amount + other.amount, this.currency);
}
public multiply(factor: number): Result<Money> {
return Money.create(this.amount * factor, this.currency);
}
}
Step 3: Update Entity to Use Value Object
interface UserProps {
email: Email;
age: Age;
balance: Money;
phone: PhoneNumber;
}
export class User extends Entity<UserProps> {
public static create(props: {
email: string;
age: number;
balance: { amount: number; currency: string };
phone: string;
}): Result<User> {
const emailOrError = Email.create(props.email);
const ageOrError = Age.create(props.age);
const balanceOrError = Money.create(props.balance.amount, props.balance.currency);
const phoneOrError = PhoneNumber.create(props.phone);
const result = Result.combine([emailOrError, ageOrError, balanceOrError, phoneOrError]);
if (result.isFailure) {
return Result.fail(result.getErrorValue());
}
return Result.ok(new User({
email: emailOrError.getValue(),
age: ageOrError.getValue(),
balance: balanceOrError.getValue(),
phone: phoneOrError.getValue(),
}));
}
get email(): Email {
return this.props.email;
}
get balance(): Money {
return this.props.balance;
}
public addBalance(amount: Money): Result<void> {
const newBalanceResult = this.balance.add(amount);
if (newBalanceResult.isFailure) {
return Result.fail(newBalanceResult.getErrorValue());
}
this.props.balance = newBalanceResult.getValue();
return Result.ok();
}
}
Step 4: Common Value Objects to Extract
- Email - email validation
- Money - amount + currency
- PhoneNumber - format validation
- Address - street, city, state, zip
- Age - range validation (0-150)
- Percentage - 0-100 validation
- DateRange - start + end dates
- URL - URL validation
- Quantity - positive integer
- Name - first + last name
Pattern: Controller Logic → Use Case
Problem: Business logic in controllers instead of use cases.
Step 1: Identify Controller with Business Logic
@Controller('orders')
export class OrderController {
@Post()
async createOrder(@Body() dto: CreateOrderDto) {
if (!dto.customerId) {
throw new BadRequestException('Customer ID required');
}
if (dto.items.length === 0) {
throw new BadRequestException('Order must have items');
}
const total = dto.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const order = await this.orderRepository.create({
customerId: dto.customerId,
items: dto.items,
total,
status: 'pending',
});
await this.emailService.sendOrderConfirmation(order.id);
return order;
}
}
Step 2: Extract to Use Case
@Injectable()
export class CreateOrderUseCase implements UseCase<CreateOrderDTO, Result<OrderDTO>> {
constructor(
@Inject('IOrderRepository') private orderRepo: IOrderRepository,
@Inject('IEmailService') private emailService: IEmailService,
) {}
async execute(dto: CreateOrderDTO): Promise<Result<OrderDTO>> {
const orderOrError = Order.create({
customerId: dto.customerId,
items: dto.items.map(item => OrderItem.create(item).getValue()),
});
if (orderOrError.isFailure) {
return Result.fail(orderOrError.getErrorValue());
}
const order = orderOrError.getValue();
const saveResult = await this.orderRepo.save(order);
if (saveResult.isFailure) {
return Result.fail('Failed to create order');
}
await this.emailService.sendOrderConfirmation(order.id.toString());
return Result.ok(OrderMapper.toDTO(order));
}
}
@Controller('orders')
export class OrderController {
constructor(private createOrderUseCase: CreateOrderUseCase) {}
@Post()
async createOrder(@Body() dto: CreateOrderDto) {
const result = await this.createOrderUseCase.execute(dto);
if (result.isFailure) {
throw new BadRequestException(result.getErrorValue());
}
return result.getValue();
}
}
Pattern: Business Logic from Use Case → Domain
Problem: Business logic in use cases instead of domain entities.
Step 1: Identify Misplaced Business Logic
class ApproveOrderUseCase {
async execute(orderId: string): Promise<Result<void>> {
const order = await this.orderRepo.findById(orderId);
if (order.total > 10000 && !order.hasManagerApproval) {
return Result.fail('Orders over $10k require manager approval');
}
if (order.items.some(item => item.quantity > 100)) {
return Result.fail('Bulk orders require special handling');
}
order.status = 'approved';
await this.orderRepo.save(order);
return Result.ok();
}
}
Step 2: Move to Domain
export class Order extends AggregateRoot<OrderProps> {
public approve(approver: User): Result<void> {
if (this.requiresManagerApproval() && !approver.isManager()) {
return Result.fail('This order requires manager approval');
}
if (this.isBulkOrder()) {
return Result.fail('Bulk orders require special approval process');
}
this.props.status = OrderStatus.Approved;
this.props.approvedBy = approver.id;
this.props.approvedAt = new Date();
this.addDomainEvent(new OrderApprovedEvent(this.id, approver.id));
return Result.ok();
}
private requiresManagerApproval(): boolean {
return this.props.total.amount > 10000;
}
private isBulkOrder(): boolean {
return this.props.items.some(item => item.quantity > 100);
}
}
class ApproveOrderUseCase {
async execute(orderId: string, userId: string): Promise<Result<void>> {
const orderResult = await this.orderRepo.findById(orderId);
const userResult = await this.userRepo.findById(userId);
if (orderResult.isFailure || userResult.isFailure) {
return Result.fail('Order or user not found');
}
const order = orderResult.getValue();
const user = userResult.getValue();
const approveResult = order.approve(user);
if (approveResult.isFailure) {
return approveResult;
}
await this.orderRepo.save(order);
return Result.ok();
}
}
Signs of Misplaced Logic:
- ❌
if statements with business rules in use cases
- ❌ Calculations in use cases
- ❌ Direct property assignments (
order.status = ...)
- ❌ Comments explaining business rules in use cases
Where Logic Should Live:
- ✅ Domain entities - Business rules, validations, calculations
- ✅ Use cases - Orchestration, fetching data, calling domain methods
- ✅ Controllers - HTTP concerns, DTO mapping, error handling
Refactoring Guidelines
General Principles:
- Read First - Understand current code structure
- Identify Pattern - Recognize anti-pattern or smell
- Plan Refactoring - Design new structure
- Write Tests - Before refactoring (if not already tested)
- Refactor - Small incremental changes
- Run Tests - Verify behavior unchanged
- Clean Up - Remove old code, update imports
Clean Architecture Rules:
- Business logic → Domain layer
- Orchestration → Use cases
- Framework concerns → Infrastructure
- Validation → Domain (factory methods)
- DTOs → Application layer
Result Pattern:
- All domain methods return
Result<T>
- All use cases return
Result<T> or Either<L, R>
- Controllers handle Result failures → HTTP exceptions
Output
Provide:
- Analysis - What's wrong with current code
- Refactored Code - Complete implementation of new pattern
- Migration Steps - How to safely apply refactoring
- Test Updates - Changes needed in tests
- Before/After Comparison - Clear visual of improvement
Make incremental, safe changes that maintain backward compatibility where possible.