# How the tcc Repository Enforces Separation of Concerns Across Domain, Application, Infrastructure, and Presentation Layers

> Discover how the tcc repository enforces separation of concerns across domain, application, infrastructure, and presentation layers using a strict onion architecture. Learn to isolate business logic and orchestrate use cases ef...

- Repository: [Lucas Zanoni⠀⠀⠀⠀⠀ ⠀╱|、 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ (˚ˎ 。7 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ |、˜〵 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ じしˍ,)ノ/tcc](https://github.com/castrozan/tcc)
- Tags: architecture
- Published: 2026-02-27

---

**The tcc codebase implements a strict onion architecture that isolates business logic in the Domain layer, orchestrates use cases in the Application layer, implements technical details in the Infrastructure layer, and handles HTTP concerns in the Presentation layer.**

The **castrozan/tcc** repository demonstrates a rigorous implementation of **separation of concerns** through a layered architecture inspired by onion and hexagonal patterns. By organizing TypeScript code into four distinct layers—Domain, Application, Infrastructure, and Presentation—the project ensures that business rules remain pure and framework-agnostic while technical dependencies point inward toward abstractions. This structure makes the system highly modular, allowing you to swap databases or web frameworks without touching core business logic.

## Domain Layer: Pure Business Logic

The **Domain layer** sits at the center of the architecture and contains zero framework dependencies. It defines what the system *is* and *does* in terms of pure business concepts.

### Entities and Value Objects

Entities in this layer encapsulate core data and invariants without any persistence knowledge. In [`src/domain/entities/professionals/Professional.ts`](https://github.com/castrozan/tcc/blob/main/src/domain/entities/professionals/Professional.ts), the `Professional` class models the essential fields and types:

```typescript
export class Professional {
    constructor(
        public id: number | null,
        public name: string,
        public role: string,
        public bio: string | null,
        public imageUrl: string | null,
        public createdAt: Date,
        public hierarchy: number
    ) {}
}

```

This entity knows nothing about databases, HTTP, or JSON serialization—it simply represents the business concept of a professional with its required attributes.

### Repository Contracts (Ports)

Instead of implementing data access, the Domain layer declares **repository interfaces** that define the contract for persistence operations. The `IProfessionalRepository` interface in [`src/domain/interfaces/professionals/IProfessionalRepository.ts`](https://github.com/castrozan/tcc/blob/main/src/domain/interfaces/professionals/IProfessionalRepository.ts) specifies what storage operations are available without dictating how they work:

```typescript
export interface IProfessionalRepository {
    create(article: Partial<Professional>): Promise<Professional>;
    findById(id: number): Promise<Professional | null>;
    findAll(): Promise<Professional[]>;
    update(article: Partial<Professional>): Promise<Professional>;
    delete(id: number): Promise<void>;
}

```

By depending on this abstraction rather than concrete database clients, the Domain layer remains isolated from infrastructure changes.

## Application Layer: Use Case Orchestration

The **Application layer** contains the *use cases* that coordinate Domain objects to fulfill specific user goals. This layer has no knowledge of HTTP, SQL, or UI frameworks—it only knows about the Domain layer and uses the repository contracts to persist data.

### Use Case Classes

Use cases encapsulate single business operations. The `CreateProfessionalUseCase` in [`src/application/use-cases/professionals/CreateProfessionalUseCase.ts`](https://github.com/castrozan/tcc/blob/main/src/application/use-cases/professionals/CreateProfessionalUseCase.ts) demonstrates this pattern by accepting a DTO, performing minimal validation, and delegating persistence to the injected repository contract:

```typescript
export class CreateProfessionalUseCase {
    constructor(private ProfessionalRepository: IProfessionalRepository) {}

    async execute(dto: CreateProfessionalDto): Promise<Professional> {
        if (!dto) {
            throw new Error('Professional not found');
        }
        return this.ProfessionalRepository.create(dto);
    }
}

```

Notice how the use case depends only on the `IProfessionalRepository` interface, not on any specific database implementation.

### Data Transfer Objects (DTOs)

DTOs define the shape of data entering and exiting the Application layer. The `CreateProfessionalDto` in [`src/application/dtos/professionals/CreateProfessionalDto.ts`](https://github.com/castrozan/tcc/blob/main/src/application/dtos/professionals/CreateProfessionalDto.ts) specifies exactly what fields are required to create a professional, acting as a boundary between external input and internal domain structures.

## Infrastructure Layer: Technical Implementation

The **Infrastructure layer** provides concrete implementations for the contracts defined in the Domain layer. This is where framework code, database clients, and external API integrations live.

### Database Repositories

Repository implementations translate between Domain entities and persistent storage. The `SQLiteProfessionalRepository` in [`src/infrastructure/database/repositories/professionals/SQLiteProfessionalRepository.ts`](https://github.com/castrozan/tcc/blob/main/src/infrastructure/database/repositories/professionals/SQLiteProfessionalRepository.ts) implements `IProfessionalRepository` using **better-sqlite3**:

```typescript
export class SQLiteProfessionalRepository implements IProfessionalRepository {
    async findById(id: number): Promise<Professional | null> {
        const db = getDatabase();
        const row = db.prepare('SELECT * FROM Professional WHERE id = ?').get(id) as ProfessionalRow | undefined;
        if (!row) return null;
        return new Professional(
            row.id,
            row.name,
            row.role,
            row.bio,
            row.imageUrl,
            new Date(row.createdAt),
            row.hierarchy
        );
    }
    // …other methods (findAll, create, update, delete) …
}

```

This class handles the messy details of SQL queries and row mapping, converting raw database results into clean `Professional` entities before returning them to the Application layer.

### Server Bootstrap

Infrastructure also contains the HTTP server configuration. The OpenAPI server bootstrap in [`src/infrastructure/web/open-api/server.ts`](https://github.com/castrozan/tcc/blob/main/src/infrastructure/web/open-api/server.ts) initializes the web framework and routes, keeping framework-specific initialization code out of Domain and Application concerns.

## Presentation Layer: HTTP Interface

The **Presentation layer** handles the "last mile" of HTTP concerns: request validation, JSON serialization, and error formatting. Controllers in this layer are intentionally thin—they translate HTTP requests into DTOs, invoke Application use cases, and format responses.

### Controllers

The `CreateProfessionalController` in [`src/presentation/controllers/professionals/CreateProfessionalController.ts`](https://github.com/castrozan/tcc/blob/main/src/presentation/controllers/professionals/CreateProfessionalController.ts) extends `OpenAPIRoute` and uses Zod schemas for validation. It demonstrates how Presentation concerns stay separate from business logic:

```typescript
export class CreateProfessionalController extends OpenAPIRoute {
    schema = { /* … OpenAPI spec generated from Zod … */ };

    @withErrorHandling
    async handle(): Promise<object> {
        const data = await this.getValidatedData<typeof this.schema>();
        const { name, role, bio, imageUrl, hierarchy } = data.body;

        const createProfessionalUseCase = new CreateProfessionalUseCase(professionalRepository);
        const professional = await createProfessionalUseCase.execute({ name, role, bio, imageUrl, hierarchy });

        return {
            success: true,
            result: {
                id: professional.id,
                name: professional.name,
                role: professional.role,
                bio: professional.bio,
                imageUrl: professional.imageUrl,
                createdAt: professional.createdAt.toISOString(),
                hierarchy: professional.hierarchy
            }
        };
    }
}

```

The controller never contains business rules; it only coordinates validation, use case execution, and response formatting.

### Error Handling Decorators

Cross-cutting concerns like error handling are extracted into decorators. The `withErrorHandling` decorator in [`src/presentation/decorators/handleErrors.ts`](https://github.com/castrozan/tcc/blob/main/src/presentation/decorators/handleErrors.ts) catches exceptions thrown by the Application layer and translates them into consistent HTTP error responses, ensuring controllers remain focused on request/response mapping.

## Layer Interaction Flow

When a client sends a POST request to create a professional, the request flows through the layers in a strict inward direction:

1. **Presentation**: The OpenAPI server (Infrastructure) routes the request to `CreateProfessionalController`, which validates the payload using Zod schemas.
2. **Application**: The controller instantiates `CreateProfessionalUseCase` with a concrete `SQLiteProfessionalRepository` and calls `execute()` with a `CreateProfessionalDto`.
3. **Domain**: The use case invokes `repository.create()`, which returns a `Professional` entity.
4. **Infrastructure**: The `SQLiteProfessionalRepository` executes SQL against the database and maps the result to a `Professional` instance.
5. **Presentation**: The controller receives the entity, formats it as JSON, and returns the HTTP response.

Because **dependency arrows always point inward** (Presentation → Application → Domain, Infrastructure → Domain), you can replace SQLite with PostgreSQL or Express with Fastify without modifying Domain or Application code.

## Summary

- The **Domain layer** (`src/domain/`) contains entities like `Professional` and repository contracts like `IProfessionalRepository`, completely free of framework dependencies.
- The **Application layer** (`src/application/`) orchestrates use cases such as `CreateProfessionalUseCase` using DTOs and repository interfaces, remaining ignorant of HTTP and database specifics.
- The **Infrastructure layer** (`src/infrastructure/`) provides concrete implementations like `SQLiteProfessionalRepository` and the OpenAPI server bootstrap, implementing Domain contracts with specific technologies.
- The **Presentation layer** (`src/presentation/`) handles HTTP concerns through thin controllers like `CreateProfessionalController` and decorators like `withErrorHandling`, translating between HTTP and Application DTOs.
- **Dependencies flow inward**, ensuring that business logic remains pure and testable while technical details remain pluggable.

## Frequently Asked Questions

### What is the difference between the Domain and Application layers?

The **Domain layer** defines *what* the business is—entities, value objects, and repository contracts—while the **Application layer** defines *how* to accomplish specific tasks by orchestrating Domain objects into use cases. Domain code contains no application logic, and Application code contains no business rules or persistence details.

### How does the Infrastructure layer implement Domain contracts without creating circular dependencies?

The Infrastructure layer depends on the Domain layer through the **Dependency Inversion Principle**. Domain defines interfaces (like `IProfessionalRepository`), and Infrastructure provides implementations (like `SQLiteProfessionalRepository`). The Application layer injects the concrete Infrastructure implementation into use cases at runtime, but compiles against Domain interfaces only, keeping the dependency graph acyclic.

### Why should business logic never leak into the Presentation layer?

Business logic in controllers leads to **code duplication** and **framework lock-in**. By keeping controllers thin—only handling HTTP validation, DTO mapping, and response formatting—you ensure that business rules live in exactly one place (the Domain layer) and can be tested without HTTP contexts or mocked frameworks.

### Can I replace SQLite with a different database without changing the Application code?

Yes. Because `CreateProfessionalUseCase` depends only on the `IProfessionalRepository` interface, you can create a new `PostgreSQLProfessionalRepository` in `src/infrastructure/database/repositories/` that implements the same interface. As long as the new repository returns `Professional` entities and honors the contract methods, the Application layer requires zero modifications.