← Blog

TypeScript Clean Architecture Generator

Generate a complete TypeScript clean architecture project from UML diagrams. Domain layer, use cases, adapters, and infrastructure—all from your models.

Open Workbench Watch Demo Start Building Free

Want to start a new TypeScript project with clean architecture already in place? This guide shows how to generate a complete layered architecture from UML diagrams—domain entities, use cases, ports, adapters, and infrastructure.

What is Clean Architecture?

Clean Architecture (popularized by Robert C. Martin) organizes code into concentric layers:

┌─────────────────────────────────────────────┐
│                Frameworks                   │  ← External (Express, React, Postgres)
├─────────────────────────────────────────────┤
│              Interface Adapters             │  ← Controllers, Gateways, Presenters
├─────────────────────────────────────────────┤
│              Application Layer              │  ← Use Cases, Application Services
├─────────────────────────────────────────────┤
│               Domain Layer                  │  ← Entities, Value Objects, Domain Services
└─────────────────────────────────────────────┘

The dependency rule: code in inner layers cannot reference outer layers. This makes the domain portable and testable.


Why Generate Clean Architecture?

Setting up clean architecture manually is tedious:

  • Creating the folder structure
  • Defining interfaces for every port
  • Wiring dependency injection
  • Maintaining consistency across layers

Model-driven generation solves this:

  1. Model your domain in a class diagram
  2. Model your use cases in sequence/activity diagrams
  3. Generate the entire project structure
  4. Start coding business logic—not boilerplate

Step 1: Model the Domain Layer

Your domain layer contains entities (things with identity) and value objects (things defined by attributes).

Example: E-commerce Domain

┌─────────────────────────────┐
│       <<entity>>            │
│         Order               │
├─────────────────────────────┤
│ - id: OrderId               │
│ - customerId: CustomerId    │
│ - items: OrderItem[]        │
│ - status: OrderStatus       │
│ - createdAt: Date           │
├─────────────────────────────┤
│ + addItem(item): void       │
│ + removeItem(id): void      │
│ + submit(): void            │
│ + cancel(): void            │
└─────────────────────────────┘

┌─────────────────────────────┐
│     <<valueObject>>         │
│       OrderItem             │
├─────────────────────────────┤
│ - productId: ProductId      │
│ - quantity: number          │
│ - price: Money              │
├─────────────────────────────┤
│ + total(): Money            │
└─────────────────────────────┘

┌─────────────────────────────┐
│     <<valueObject>>         │
│         Money               │
├─────────────────────────────┤
│ - amount: number            │
│ - currency: Currency        │
├─────────────────────────────┤
│ + add(m: Money): Money      │
│ + multiply(n): Money        │
└─────────────────────────────┘

Generated Domain Code

// src/domain/entities/Order.ts
import { Entity } from '../core/Entity';
import { OrderId } from '../valueObjects/OrderId';
import { CustomerId } from '../valueObjects/CustomerId';
import { OrderItem } from '../valueObjects/OrderItem';
import { OrderStatus } from '../valueObjects/OrderStatus';

interface OrderProps {
  customerId: CustomerId;
  items: OrderItem[];
  status: OrderStatus;
  createdAt: Date;
}

export class Order extends Entity<OrderId, OrderProps> {
  get customerId(): CustomerId {
    return this.props.customerId;
  }

  get items(): OrderItem[] {
    return [...this.props.items];
  }

  get status(): OrderStatus {
    return this.props.status;
  }

  addItem(item: OrderItem): void {
    if (this.props.status !== OrderStatus.DRAFT) {
      throw new Error('Cannot modify submitted order');
    }
    this.props.items.push(item);
  }

  removeItem(productId: ProductId): void {
    if (this.props.status !== OrderStatus.DRAFT) {
      throw new Error('Cannot modify submitted order');
    }
    this.props.items = this.props.items.filter(
      i => !i.productId.equals(productId)
    );
  }

  submit(): void {
    if (this.props.items.length === 0) {
      throw new Error('Cannot submit empty order');
    }
    this.props.status = OrderStatus.SUBMITTED;
  }

  cancel(): void {
    if (this.props.status === OrderStatus.SHIPPED) {
      throw new Error('Cannot cancel shipped order');
    }
    this.props.status = OrderStatus.CANCELLED;
  }
}
// src/domain/valueObjects/Money.ts
import { ValueObject } from '../core/ValueObject';
import { Currency } from './Currency';

interface MoneyProps {
  amount: number;
  currency: Currency;
}

export class Money extends ValueObject<MoneyProps> {
  get amount(): number {
    return this.props.amount;
  }

  get currency(): Currency {
    return this.props.currency;
  }

  add(other: Money): Money {
    if (!this.currency.equals(other.currency)) {
      throw new Error('Cannot add different currencies');
    }
    return new Money({
      amount: this.amount + other.amount,
      currency: this.currency
    });
  }

  multiply(factor: number): Money {
    return new Money({
      amount: this.amount * factor,
      currency: this.currency
    });
  }

  protected validate(): void {
    if (this.props.amount < 0) {
      throw new Error('Money amount cannot be negative');
    }
  }
}

Step 2: Model Use Cases with Sequence Diagrams

Use cases orchestrate domain objects. Model them as sequence diagrams:

Controller      CreateOrderUseCase     OrderRepository     EventPublisher
    │                  │                     │                   │
    │── execute(dto) ──▶                     │                   │
    │                  │── create(props) ───▶│                   │
    │                  │◀─── order ──────────│                   │
    │                  │── validate() ───────┤                   │
    │                  │── save(order) ──────▶                   │
    │                  │                     │                   │
    │                  │── publish(OrderCreated) ───────────────▶│
    │◀─── Result ──────│                     │                   │

Generated Use Case

// src/application/useCases/CreateOrderUseCase.ts
import { UseCase } from '../core/UseCase';
import { Order } from '../../domain/entities/Order';
import { OrderRepository } from '../ports/OrderRepository';
import { EventPublisher } from '../ports/EventPublisher';
import { CreateOrderDTO } from '../dtos/CreateOrderDTO';
import { OrderCreatedEvent } from '../../domain/events/OrderCreatedEvent';

export class CreateOrderUseCase implements UseCase<CreateOrderDTO, Order> {
  constructor(
    private readonly orderRepository: OrderRepository,
    private readonly eventPublisher: EventPublisher
  ) {}

  async execute(dto: CreateOrderDTO): Promise<Order> {
    // Create domain entity
    const order = Order.create({
      customerId: new CustomerId(dto.customerId),
      items: dto.items.map(item => OrderItem.create({
        productId: new ProductId(item.productId),
        quantity: item.quantity,
        price: new Money({ amount: item.price, currency: Currency.USD })
      })),
      status: OrderStatus.DRAFT,
      createdAt: new Date()
    });

    // Persist
    await this.orderRepository.save(order);

    // Publish domain event
    await this.eventPublisher.publish(
      new OrderCreatedEvent(order.id, order.customerId)
    );

    return order;
  }
}

Step 3: Define Ports (Interfaces)

Ports are the contracts between your application and the outside world:

// src/application/ports/OrderRepository.ts
import { Order } from '../../domain/entities/Order';
import { OrderId } from '../../domain/valueObjects/OrderId';

export interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: OrderId): Promise<Order | null>;
  findByCustomerId(customerId: CustomerId): Promise<Order[]>;
  delete(id: OrderId): Promise<void>;
}

// src/application/ports/EventPublisher.ts
import { DomainEvent } from '../../domain/events/DomainEvent';

export interface EventPublisher {
  publish<T extends DomainEvent>(event: T): Promise<void>;
}

Step 4: Generate Adapters

Adapters implement ports for specific technologies:

// src/adapters/persistence/PostgresOrderRepository.ts
import { Pool } from 'pg';
import { Order } from '../../domain/entities/Order';
import { OrderRepository } from '../../application/ports/OrderRepository';
import { OrderMapper } from './mappers/OrderMapper';

export class PostgresOrderRepository implements OrderRepository {
  constructor(private readonly pool: Pool) {}

  async save(order: Order): Promise<void> {
    const data = OrderMapper.toPersistence(order);
    await this.pool.query(
      `INSERT INTO orders (id, customer_id, items, status, created_at)
       VALUES ($1, $2, $3, $4, $5)
       ON CONFLICT (id) DO UPDATE SET
         items = EXCLUDED.items,
         status = EXCLUDED.status`,
      [data.id, data.customerId, JSON.stringify(data.items), data.status, data.createdAt]
    );
  }

  async findById(id: OrderId): Promise<Order | null> {
    const result = await this.pool.query(
      'SELECT * FROM orders WHERE id = $1',
      [id.value]
    );
    if (result.rows.length === 0) return null;
    return OrderMapper.toDomain(result.rows[0]);
  }

  async findByCustomerId(customerId: CustomerId): Promise<Order[]> {
    const result = await this.pool.query(
      'SELECT * FROM orders WHERE customer_id = $1 ORDER BY created_at DESC',
      [customerId.value]
    );
    return result.rows.map(OrderMapper.toDomain);
  }

  async delete(id: OrderId): Promise<void> {
    await this.pool.query('DELETE FROM orders WHERE id = $1', [id.value]);
  }
}

Generated Project Structure

Here’s the complete generated structure:

src/
├── domain/
│   ├── core/
│   │   ├── Entity.ts
│   │   ├── ValueObject.ts
│   │   ├── AggregateRoot.ts
│   │   └── DomainEvent.ts
│   ├── entities/
│   │   ├── Order.ts
│   │   ├── Customer.ts
│   │   └── Product.ts
│   ├── valueObjects/
│   │   ├── OrderId.ts
│   │   ├── CustomerId.ts
│   │   ├── Money.ts
│   │   └── OrderStatus.ts
│   ├── events/
│   │   ├── OrderCreatedEvent.ts
│   │   └── OrderShippedEvent.ts
│   └── services/
│       └── PricingService.ts

├── application/
│   ├── core/
│   │   └── UseCase.ts
│   ├── ports/
│   │   ├── OrderRepository.ts
│   │   ├── CustomerRepository.ts
│   │   ├── EventPublisher.ts
│   │   └── PaymentGateway.ts
│   ├── useCases/
│   │   ├── CreateOrderUseCase.ts
│   │   ├── SubmitOrderUseCase.ts
│   │   └── CancelOrderUseCase.ts
│   └── dtos/
│       ├── CreateOrderDTO.ts
│       └── OrderResponseDTO.ts

├── adapters/
│   ├── persistence/
│   │   ├── PostgresOrderRepository.ts
│   │   └── mappers/
│   │       └── OrderMapper.ts
│   ├── messaging/
│   │   └── RabbitMQEventPublisher.ts
│   └── payment/
│       └── StripePaymentGateway.ts

└── infrastructure/
    ├── http/
    │   ├── controllers/
    │   │   └── OrderController.ts
    │   └── middleware/
    │       └── authMiddleware.ts
    ├── config/
    │   └── database.ts
    └── di/
        └── container.ts

Dependency Injection Setup

The generator also creates DI container wiring:

// src/infrastructure/di/container.ts
import { Container } from 'inversify';
import { Pool } from 'pg';

// Ports
import { OrderRepository } from '../../application/ports/OrderRepository';
import { EventPublisher } from '../../application/ports/EventPublisher';

// Adapters
import { PostgresOrderRepository } from '../../adapters/persistence/PostgresOrderRepository';
import { RabbitMQEventPublisher } from '../../adapters/messaging/RabbitMQEventPublisher';

// Use Cases
import { CreateOrderUseCase } from '../../application/useCases/CreateOrderUseCase';

const container = new Container();

// Infrastructure
container.bind<Pool>('Pool').toConstantValue(new Pool());

// Ports → Adapters
container.bind<OrderRepository>('OrderRepository').to(PostgresOrderRepository);
container.bind<EventPublisher>('EventPublisher').to(RabbitMQEventPublisher);

// Use Cases
container.bind<CreateOrderUseCase>('CreateOrderUseCase').to(CreateOrderUseCase);

export { container };

Testing Clean Architecture

Each layer has different testing strategies:

Domain Layer (Unit Tests)

describe('Order', () => {
  it('should add item to draft order', () => {
    const order = Order.create({ /* ... */ });
    order.addItem(OrderItem.create({ /* ... */ }));
    expect(order.items).toHaveLength(1);
  });

  it('should not allow adding items to submitted order', () => {
    const order = Order.create({ /* ... */ });
    order.submit();
    expect(() => order.addItem(/* ... */)).toThrow();
  });
});

Application Layer (Integration Tests with Mocks)

describe('CreateOrderUseCase', () => {
  it('should create and persist order', async () => {
    const mockRepo = { save: jest.fn() };
    const mockPublisher = { publish: jest.fn() };
    
    const useCase = new CreateOrderUseCase(mockRepo, mockPublisher);
    const result = await useCase.execute({ /* dto */ });

    expect(mockRepo.save).toHaveBeenCalledWith(expect.any(Order));
    expect(mockPublisher.publish).toHaveBeenCalledWith(expect.any(OrderCreatedEvent));
  });
});

Next Steps

Ready to scaffold your TypeScript project? Start building free.