The problem with "just ship it"
Early in my career, I wrote a lot of code that worked but was painful to change. Features were tangled together, business logic lived inside HTTP handlers, and testing meant spinning up an entire database.
Clean Architecture โ popularized by Robert C. Martin โ gave me a mental model to fix this.
The core idea
The dependency rule is simple: source code dependencies must point inward. Inner layers know nothing about outer layers.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frameworks โ โ Outermost: Express, React, PostgreSQL
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Interface Adapters โ โ Controllers, Presenters, Gateways
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Use Cases โ โ Application-specific business rules
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Entities โ โ Enterprise-wide business rules
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
A real example
Let's say we're building a user registration feature. Here's how I'd structure it:
// entities/User.ts โ pure business logic, no dependencies
export class User {
constructor(
public readonly id: string,
public readonly email: string,
public readonly name: string,
private passwordHash: string,
) {}
static create(email: string, name: string, hash: string): User {
if (!email.includes('@')) throw new Error('Invalid email');
return new User(crypto.randomUUID(), email, name, hash);
}
}
// usecases/RegisterUser.ts โ orchestrates the flow
export class RegisterUser {
constructor(
private userRepo: UserRepository,
private hasher: PasswordHasher,
) {}
async execute(input: { email: string; name: string; password: string }) {
const existing = await this.userRepo.findByEmail(input.email);
if (existing) throw new Error('Email already registered');
const hash = await this.hasher.hash(input.password);
const user = User.create(input.email, input.name, hash);
await this.userRepo.save(user);
return user;
}
}
Notice how RegisterUser depends on interfaces (UserRepository, PasswordHasher), not concrete implementations. This makes testing trivial โ just pass in mocks.
Lessons learned
- Don't over-architect small projects. A weekend hackathon doesn't need four layers of abstraction.
- Start with use cases. Write the business logic first, then figure out the adapters.
- Interfaces are your best friend. They make the dependency rule easy to follow.
- It's okay to bend the rules. Pragmatism beats purity.
Wrapping up
Clean Architecture isn't a silver bullet, but it's a powerful tool for managing complexity. The key insight is simple: protect your business logic from infrastructure details.
If you're building something that needs to last longer than a sprint, it's worth the investment.