Clean Architecture, SOLID e os padrões que separam código que dura de código que vira dívida. Sem dogmatismo — cada padrão tem contexto e custo.
"A melhor arquitetura é a que funciona, não a que está na moda. Pragmatismo antes de purismo."
— Cesar Zanis
01
SOLID — os que mais importam no dia a dia
Os 5 princípios existem. Mas S, O e D são os que você vai usar toda semana. L e I são importantes, mas violados com muito menos frequência.
S
Single Responsibility
Uma classe, um motivo para mudar.
Uma classe que envia e-mail, valida dados E salva no banco vai te dar dor de cabeça na hora de testar e dobro de dor na hora de mudar. Separe responsabilidades pelo motivo de mudança, não pelo tamanho.
// ✅ Cada classe com um motivo para existir
class OrderValidator { validate(order): Result<void> }
class OrderRepository { save(order): Promise<void> }
class OrderNotifier { notify(order): Promise<void> }
class OrderPdfGenerator { generate(order): Buffer }
O
Open/Closed
Aberto para extensão, fechado para modificação.
Você não devia precisar editar uma classe existente toda vez que um novo tipo de pagamento aparece. Use interfaces e polimorfismo para estender comportamento sem tocar no que já funciona.
Problemático
// ❌ Cada novo método de pagamento exige editar essa classe
class PaymentProcessor {
process(method: string, amount: number) {
if (method === 'pix') { /* ... */ }
else if (method === 'credit') { /* ... */ }
else if (method === 'boleto') { /* ... */ }
// +1 if para cada novo método — risco de quebrar o que funciona
}
}
Correto
// ✅ Novo método = nova classe, sem tocar no core
interface PaymentStrategy {
process(amount: number): Promise<PaymentResult>
}
class PixPayment implements PaymentStrategy { ... }
class CreditCardPayment implements PaymentStrategy { ... }
class BoletoPayment implements PaymentStrategy { ... }
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
process(amount: number) { return this.strategy.process(amount) }
}
D
Dependency Inversion
Dependa de abstrações, nunca de implementações.
Seu serviço de pedidos não deveria importar o Stripe diretamente. Ele deveria depender de uma interface IPaymentGateway. Isso permite trocar Stripe por PagSeguro sem tocar na lógica de negócio — e testar sem fazer chamada real.
Problemático
// ❌ Acoplado à implementação — impossível testar sem chamar o Stripe
class OrderService {
private stripe = new StripeClient(process.env.STRIPE_KEY)
async charge(order: Order) {
return this.stripe.charges.create({ amount: order.total })
}
}
Correto
// ✅ Depende da abstração — testável, trocável
interface IPaymentGateway {
charge(amount: number, currency: string): Promise<ChargeResult>
}
class OrderService {
constructor(private payment: IPaymentGateway) {}
async charge(order: Order) {
return this.payment.charge(order.total, order.currency)
}
}
// Em produção: new OrderService(new StripeGateway())
// Em teste: new OrderService(new MockPaymentGateway())
02
Padrões que resolvem problemas reais
Result Pattern — erros sem exceção
O padrão que mais melhora legibilidade em APIs complexas.
Usar exceção para fluxo de negócio (email já cadastrado, saldo insuficiente) é abuso de exceção. Exceção é para o inesperado — banco caiu, disco cheio. Para fluxo de negócio, use Result<T>: retorna sucesso ou falha de forma explícita, forçando o chamador a lidar com ambos os casos.
TypeScript
// O tipo
type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E }
// O serviço — sem throw para fluxo de negócio
class UserService {
async register(email: string, password: string): Promise<Result<User>> {
const exists = await this.repo.findByEmail(email)
if (exists) {
return { ok: false, error: 'EMAIL_ALREADY_REGISTERED' }
}
const user = await this.repo.create({ email, password: await hash(password) })
return { ok: true, value: user }
}
}
// O controller — forçado a lidar com o erro
async register(req, res) {
const result = await this.userService.register(req.body.email, req.body.password)
if (!result.ok) {
return res.status(409).json({ error: result.error })
}
return res.status(201).json(result.value)
}
Repository Pattern — banco como detalhe
A camada de dados não é o seu domínio. Trate como detalhe.
Seu serviço de negócio não deve saber se os dados vêm de PostgreSQL, Redis ou uma API externa. O Repository abstrai isso. Troca o banco sem tocar na lógica. Testa a lógica sem banco real.
TypeScript
// A interface — no domínio, sem dependência de banco
interface IOrderRepository {
findById(id: string): Promise<Order | null>
findByCustomer(customerId: string): Promise<Order[]>
save(order: Order): Promise<void>
delete(id: string): Promise<void>
}
// A implementação — detalhe de infraestrutura
class PostgresOrderRepository implements IOrderRepository {
async findById(id: string): Promise<Order | null> {
const row = await this.db.query(
'SELECT * FROM orders WHERE public_id = $1 AND deleted_at IS NULL',
[id]
)
return row ? OrderMapper.toDomain(row) : null
}
// ...
}
// O serviço — completamente agnóstico de banco
class OrderService {
constructor(private orders: IOrderRepository) {}
async cancel(orderId: string): Promise<Result<void>> {
const order = await this.orders.findById(orderId)
if (!order) return { ok: false, error: 'ORDER_NOT_FOUND' }
order.cancel() // lógica de domínio
await this.orders.save(order)
return { ok: true, value: undefined }
}
}
03
OWASP Top 10 — os que mais aparecem
Segurança não é feature — é ausência de falha. Os 5 abaixo aparecem em 80% dos relatórios de pentest.
A01
Broken Access Control
Validar permissão no backend em TODA requisição. Nunca confie no frontend para controle de acesso. Teste com usuário sem permissão tentando acessar recurso de outro.
A02
Cryptographic Failures
Bcrypt ou Argon2 para senhas — nunca MD5 ou SHA sem salt. TLS em todas as comunicações. Nunca logue dados sensíveis (tokens, senhas, CPF).
A03
SQL Injection
Queries parametrizadas sempre. Nunca concatene input do usuário em SQL. ORMs ajudam mas não são bala de prata — revise queries geradas.
A07
Auth Failures
JWT com expiração curta (15-60min) + refresh token com rotação. Rate limiting em /login. Não retorne se o email existe ou não em "esqueci a senha".
A09
Logging inadequado
Logue o suficiente para reconstruir o incidente. Nunca logue o dado — logue o evento. "Usuário X acessou recurso Y", nunca o conteúdo do recurso.
Regra prática: antes de aplicar qualquer padrão, faça a pergunta — "qual problema concreto isso resolve hoje?" Se a resposta for "nenhum, mas pode ser útil no futuro", você está adicionando complexidade antecipada. Espere o problema aparecer.