Architecture · September 8, 2026 · 9 min read

Clean Architecture Without the Ceremony

How I apply dependency inversion and layered boundaries on real projects without drowning a small codebase in folders.

DA

Dimas Adit Thalia Putra

Fullstack Developer

Clean Architecture Without the Ceremony

Clean Architecture has a reputation for producing codebases with twelve folders and one actual line of logic. That is a misuse of the idea, not the idea itself. The core principle is small: source code dependencies point inward, toward business rules.

The one rule that matters

Your business logic should not know whether it is called from an HTTP controller, a queue worker or a test. It should also not know whether data lives in PostgreSQL or an in-memory array.

In practice this means separating three concerns:

  • Domain: entities and rules that exist regardless of technology
  • Application: use cases that orchestrate the domain
  • Infrastructure: frameworks, databases, HTTP, third-party APIs

The domain and application layers never import from infrastructure. Infrastructure imports from them.

A concrete example

Take a payroll system. Computing a net salary is a business rule. Loading employees from MySQL is an implementation detail.

// application/ports.ts
export interface EmployeeRepository {
  findById(id: string): Promise<Employee | null>;
  savePayslip(payslip: Payslip): Promise<void>;
}

// application/generate-payslip.ts
export class GeneratePayslip {
  constructor(private readonly employees: EmployeeRepository) {}

  async execute(employeeId: string, period: Period): Promise<Payslip> {
    const employee = await this.employees.findById(employeeId);
    if (!employee) throw new EmployeeNotFound(employeeId);

    const payslip = employee.calculatePayslip(period);
    await this.employees.savePayslip(payslip);
    return payslip;
  }
}

The use case depends on an interface it owns. The database-backed implementation lives in infrastructure and is wired in at the edge of the application.

What you gain

  • Testability. The use case runs in milliseconds with a fake repository. No database, no framework boot.
  • Replaceable details. Swapping a queue provider or moving a report to a read replica touches one adapter.
  • Readable intent. Opening the application folder tells you what the system does, not which framework it uses.

Applying it in Laravel

Laravel is built around Eloquent and facades, which pull you toward the active-record style. You do not have to fight the framework everywhere. A pragmatic split that has worked for me:

  • Keep simple CRUD in controllers and models. Not every screen is a domain.
  • Extract an action or service class once a workflow has real rules, several steps or multiple entry points
  • Introduce a repository interface only when you genuinely need to isolate persistence, such as complex queries or tests that should not hit the database
  • Keep validation in form requests and business rules in the action, never in the controller
Architecture should scale with complexity. A settings page does not need a use case; a payroll run does.

Common mistakes

  • Interfaces for everything. An interface with a single implementation and no reason to change is noise.
  • Anemic domain models. If entities are just bags of properties and every rule lives in services, the domain layer adds nothing.
  • Leaking framework types inward. The moment a use case accepts an HTTP request object, the boundary is gone.
  • Copying the folder structure instead of the principle. Directory names do not enforce dependency direction. Imports do.

A quick way to check yourself

Ask whether you could run your core logic from a script with no framework loaded. If yes, your boundaries are probably healthy. If it needs the whole application container to run, the dependencies are pointing the wrong way.

Start with the rule, add structure when the code asks for it, and resist structure the code has not earned.

  • #Clean Architecture
  • #Laravel
  • #TypeScript

Keep reading

Portfolio©2026

DIMAS

Loading the experience

000