arch

Arquitectura

Referencias de diseño de sistemas, patrones y decisiones técnicas.

Clean Architecture

Clean Architecture organiza o código em camadas concêntricas onde as dependências sempre apontam para dentro, do framework para o domínio, nunca o contrário.

Camadas

  • Entities: regras de negócio puras, sem dependências externas
  • Use Cases: orquestra as entidades, define o que o sistema faz
  • Interface Adapters: controllers, presenters, gateways
  • Frameworks & Drivers: banco, UI, APIs externas
src/domain/entities/User.ts
// Entidade pura, sem dependência de framework
export class User {
  constructor(
    public readonly id: string,
    public readonly email: string,
    private readonly passwordHash: string
  ) {}

  isPasswordValid(hash: string): boolean {
    return this.passwordHash === hash;
  }
}

// Use Case, depende apenas da entidade e do repositório (interface)
export class GetUserByIdUseCase {
  constructor(private readonly repo: UserRepository) {}

  async execute(id: string): Promise<User | null> {
    return this.repo.findById(id);
  }
}
src/domain/repositories/UserRepository.ts
// Interface, definida no domínio, implementada na camada externa
export interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

💡 Dica

A regra de ouro: dependências de código-fonte apontam apenas para dentro. O domínio não importa nada externo.

SOLID

Os cinco princípios que tornam software orientado a objetos mais compreensível, flexível e manutenível.

  • S : Single Responsibility: cada classe tem uma razão para mudar
  • O : Open/Closed: aberto para extensão, fechado para modificação
  • L : Liskov Substitution: subtipos devem ser substituíveis por seus tipos base
  • I  : Interface Segregation: interfaces específicas são melhores que uma geral
  • D : Dependency Inversion: dependa de abstrações, não de concretizações
solid/dependency-inversion.ts
// ❌ Depende de implementação concreta
class OrderService {
  private mailer = new SendgridMailer(); // acoplado
}

// ✅ Depende de abstração
interface Mailer {
  send(to: string, subject: string, body: string): Promise<void>;
}

class OrderService {
  constructor(private readonly mailer: Mailer) {} // injeção de dependência

  async confirmOrder(order: Order) {
    await this.mailer.send(order.email, "Confirmação", `Pedido #${order.id} confirmado`);
  }
}

Design Patterns

Padrões de projeto são soluções reutilizáveis para problemas recorrentes de design de software.

data

Repository

Abstrai o acesso a dados. O domínio usa uma interface; a implementação pode ser SQL, MongoDB, in-memory.

creational

Factory

Encapsula a criação de objetos complexos. Útil quando a criação depende de condições ou configurações.

behavioral

Observer

Objetos observam mudanças em outro objeto. Base de eventos, pub/sub, reactive programming.

behavioral

Strategy

Define uma família de algoritmos e os torna intercambiáveis sem alterar o cliente.

structural

Decorator

Adiciona responsabilidades a um objeto dinamicamente. Alternativa limpa à herança excessiva.

structural

Adapter

Converte a interface de uma classe em outra esperada pelo cliente. Integração de sistemas legados.

patterns/repository.ts
// Repository Pattern, desacopla o domínio do banco
interface ProductRepository {
  findAll(): Promise<Product[]>;
  findById(id: string): Promise<Product | null>;
  save(product: Product): Promise<void>;
  delete(id: string): Promise<void>;
}

// Implementação PostgreSQL
class PostgresProductRepository implements ProductRepository {
  constructor(private readonly db: Database) {}

  async findAll() {
    const rows = await this.db.query("SELECT * FROM products");
    return rows.map(toProduct);
  }

  async findById(id: string) {
    const row = await this.db.queryOne("SELECT * FROM products WHERE id = $1", [id]);
    return row ? toProduct(row) : null;
  }
  // ...
}

// Implementação in-memory (para testes)
class InMemoryProductRepository implements ProductRepository {
  private store = new Map<string, Product>();

  async findAll() { return [...this.store.values()]; }
  async findById(id: string) { return this.store.get(id) ?? null; }
  async save(product: Product) { this.store.set(product.id, product); }
  async delete(id: string) { this.store.delete(id); }
}

Microservices vs Monolith

Monolito Modular

Um único deploy com fronteiras de módulo bem definidas internamente. Ponto de partida recomendado.

  • Desenvolvimento e deploy simples
  • Transações ACID nativas
  • Fácil de refatorar e testar
  • Escala vertical eficiente até certo ponto

Microserviços

Serviços independentes com responsabilidades bem definidas. Justificado quando há necessidades reais de escala ou times grandes.

  • Deploy independente por serviço
  • Escala granular por serviço
  • Complexidade de rede e consistência eventual
  • Overhead operacional elevado

⚠️ Atenção

Comece com monolito. Microserviços resolvem problemas de escala de time e serviço, não de código mal organizado.

Deploy & Infra

Estratégias de Deploy

  • Blue/Green: dois ambientes idênticos, troca de tráfego atômica
  • Canary: rollout gradual para % dos usuários
  • Rolling: atualiza instâncias gradualmente, sem downtime
  • Feature Flags: deploy desacoplado de release, código no ar, feature desabilitada

Este site

Exportação estática com next build → S3 + CloudFront. Zero servidor em runtime. Deploy via aws s3 sync + invalidação de CDN.

deploy-to-s3.sh
#!/bin/bash
# Build estático
npm run build

# Sync para S3
aws s3 sync ./out s3://${BUCKET_NAME} \
  --delete \
  --cache-control "public, max-age=31536000, immutable" \
  --exclude "*.html"

aws s3 sync ./out s3://${BUCKET_NAME} \
  --exclude "*" \
  --include "*.html" \
  --cache-control "no-cache, no-store"

# Invalida cache do CloudFront
aws cloudfront create-invalidation \
  --distribution-id ${CF_DIST_ID} \
  --paths "/*"