Skip to content

Latest commit

 

History

History
679 lines (576 loc) · 23.1 KB

File metadata and controls

679 lines (576 loc) · 23.1 KB

Capítulo 18: Anatomia de um Holon

"Conhece-te a ti mesmo." — Inscrição no Templo de Delfos

18.1 A Estrutura Viva

Um holon de software é como um organismo vivo: possui órgãos especializados que trabalham em harmonia, fronteiras que o definem, e mecanismos de autopreservação. Vamos dissecar essa anatomia.

┌─────────────────────────────────────────────────────────────────────────┐
│                            HOLON: PEDIDOS                                │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                     INTERFACE PÚBLICA (Pele)                     │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │   │
│  │  │  REST API   │  │  GraphQL    │  │    Event Handlers       │  │   │
│  │  │  /pedidos   │  │  /graphql   │  │    @OnEvent(...)        │  │   │
│  │  └─────────────┘  └─────────────┘  └─────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                    │                                    │
│  ┌─────────────────────────────────▼───────────────────────────────┐   │
│  │                   NÚCLEO DE DOMÍNIO (Cérebro)                    │   │
│  │  ┌───────────────────────────────────────────────────────────┐  │   │
│  │  │                      Use Cases                             │  │   │
│  │  │    CriarPedido  ConfirmarPedido  CancelarPedido            │  │   │
│  │  └───────────────────────────────────────────────────────────┘  │   │
│  │  ┌───────────────────────────────────────────────────────────┐  │   │
│  │  │                      Entities                              │  │   │
│  │  │    Pedido  ItemPedido  Cliente(réplica)                    │  │   │
│  │  └───────────────────────────────────────────────────────────┘  │   │
│  │  ┌───────────────────────────────────────────────────────────┐  │   │
│  │  │                   Value Objects                            │  │   │
│  │  │    Money  PedidoStatus  EnderecoEntrega                    │  │   │
│  │  └───────────────────────────────────────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                    │                                    │
│  ┌─────────────────────────────────▼───────────────────────────────┐   │
│  │                    PERSISTÊNCIA (Memória)                        │   │
│  │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │   │
│  │  │ Dados Próprios  │  │    Réplicas     │  │     Cache       │  │   │
│  │  │  PostgreSQL     │  │  Clientes(r/o)  │  │     Redis       │  │   │
│  │  │  (autoridade)   │  │  Produtos(r/o)  │  │   (efêmero)     │  │   │
│  │  └─────────────────┘  └─────────────────┘  └─────────────────┘  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                    │                                    │
│  ┌─────────────────────────────────▼───────────────────────────────┐   │
│  │                AUTO-REGULAÇÃO (Sistema Imune)                    │   │
│  │  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌─────────────┐  │   │
│  │  │  Health   │  │  Circuit  │  │ Fallbacks │  │ Observabil. │  │   │
│  │  │  Checks   │  │  Breakers │  │           │  │ (logs,métr) │  │   │
│  │  └───────────┘  └───────────┘  └───────────┘  └─────────────┘  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

18.2 Interface Pública (A Pele)

A interface pública é como a pele de um organismo: protege o interior, controla o que entra e sai, e apresenta uma "face" para o mundo exterior.

APIs de Entrada

// infrastructure/http/pedido.controller.ts
@Controller('pedidos')
@ApiTags('Pedidos')
export class PedidoController {
  constructor(
    private readonly criarPedido: CriarPedidoUseCase,
    private readonly buscarPedido: BuscarPedidoUseCase,
    private readonly confirmarPedido: ConfirmarPedidoUseCase
  ) {}

  @Post()
  @ApiOperation({ summary: 'Cria um novo pedido' })
  @ApiResponse({ status: 201, type: PedidoResponseDTO })
  async criar(
    @Body() dto: CriarPedidoDTO,
    @Headers('X-Correlation-Id') correlationId: string
  ): Promise<PedidoResponseDTO> {
    // Controller é fino: apenas traduz HTTP para Use Case
    const resultado = await this.criarPedido.execute({
      clienteId: dto.clienteId,
      itens: dto.itens,
      correlationId
    });

    return PedidoResponseDTO.fromDomain(resultado);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Busca pedido por ID' })
  async buscar(@Param('id') id: string): Promise<PedidoResponseDTO> {
    const pedido = await this.buscarPedido.execute(id);

    if (!pedido) {
      throw new NotFoundException(`Pedido ${id} não encontrado`);
    }

    return PedidoResponseDTO.fromDomain(pedido);
  }

  @Patch(':id/confirmar')
  @ApiOperation({ summary: 'Confirma um pedido pendente' })
  async confirmar(@Param('id') id: string): Promise<PedidoResponseDTO> {
    const pedido = await this.confirmarPedido.execute(id);
    return PedidoResponseDTO.fromDomain(pedido);
  }
}

DTOs: A Linguagem da Fronteira

// infrastructure/http/dtos/criar-pedido.dto.ts
import { IsUUID, IsArray, ValidateNested, IsPositive, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

export class ItemPedidoDTO {
  @ApiProperty({ example: 'prod-123' })
  @IsUUID()
  produtoId: string;

  @ApiProperty({ example: 'Camiseta Azul M' })
  nome: string;

  @ApiProperty({ example: 2, minimum: 1 })
  @IsPositive()
  @Min(1)
  quantidade: number;

  @ApiProperty({ example: 99.90 })
  @IsPositive()
  precoUnitario: number;
}

export class CriarPedidoDTO {
  @ApiProperty({ example: 'cli-456' })
  @IsUUID()
  clienteId: string;

  @ApiProperty({ type: [ItemPedidoDTO] })
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => ItemPedidoDTO)
  itens: ItemPedidoDTO[];
}

// Response DTO — o que sai do holon
export class PedidoResponseDTO {
  id: string;
  clienteId: string;
  itens: ItemPedidoDTO[];
  total: string;  // Formatado: "R$ 199,80"
  status: string;
  criadoEm: string;

  static fromDomain(pedido: Pedido): PedidoResponseDTO {
    return {
      id: pedido.id,
      clienteId: pedido.clienteId,
      itens: pedido.itens.map(i => ({
        produtoId: i.produtoId,
        nome: i.nome,
        quantidade: i.quantidade,
        precoUnitario: i.precoUnitario.valor
      })),
      total: pedido.calcularTotal().format(),
      status: pedido.status,
      criadoEm: pedido.criadoEm.toISOString()
    };
  }
}

Event Handlers: Ouvindo o Mundo

// infrastructure/messaging/event-handlers/cliente-atualizado.handler.ts
@Injectable()
export class ClienteAtualizadoHandler {
  constructor(
    private readonly clienteReplica: ClienteReplicaRepository,
    private readonly logger: Logger
  ) {}

  @OnEvent('ClienteAtualizado')
  async handle(evento: ClienteAtualizadoEvent): Promise<void> {
    const span = tracer.startSpan('handle-cliente-atualizado');

    try {
      await this.clienteReplica.sincronizar({
        clienteId: evento.clienteId,
        nome: evento.nome,
        email: evento.email,
        endereco: evento.endereco
      });

      this.logger.log(`Réplica do cliente ${evento.clienteId} atualizada`);
    } catch (error) {
      this.logger.error(`Falha ao sincronizar cliente ${evento.clienteId}`, error);
      // Não relança — evento será reprocessado pela DLQ se necessário
    } finally {
      span.end();
    }
  }
}

18.3 Núcleo de Domínio (O Cérebro)

O núcleo de domínio é onde reside a inteligência do holon. Aqui estão as regras de negócio, as entidades, e os casos de uso.

Entidades Ricas

// domain/entities/pedido.entity.ts
export class Pedido {
  private eventos: DomainEvent[] = [];

  private constructor(
    public readonly id: string,
    public readonly clienteId: string,
    private _itens: ItemPedido[],
    private _status: PedidoStatus,
    private _enderecoEntrega: EnderecoEntrega | null,
    public readonly criadoEm: Date,
    private _atualizadoEm: Date
  ) {}

  // Factory method — única forma de criar
  static criar(props: CriarPedidoProps): Pedido {
    const pedido = new Pedido(
      randomUUID(),
      props.clienteId,
      props.itens.map(ItemPedido.criar),
      PedidoStatus.PENDENTE,
      null,
      new Date(),
      new Date()
    );

    pedido.addEvento(new PedidoCriadoEvent(pedido));
    return pedido;
  }

  // Reconstituição do banco — sem eventos
  static reconstituir(props: ReconstituirPedidoProps): Pedido {
    return new Pedido(
      props.id,
      props.clienteId,
      props.itens,
      props.status,
      props.enderecoEntrega,
      props.criadoEm,
      props.atualizadoEm
    );
  }

  // Métodos de negócio — encapsulam regras
  adicionarItem(item: ItemPedido): void {
    if (this._status !== PedidoStatus.PENDENTE) {
      throw new PedidoNaoPermiteAlteracaoError(this.id, this._status);
    }

    const existente = this._itens.find(i => i.produtoId === item.produtoId);
    if (existente) {
      existente.aumentarQuantidade(item.quantidade);
    } else {
      this._itens.push(item);
    }

    this.atualizarTimestamp();
    this.addEvento(new ItemAdicionadoAoPedidoEvent(this.id, item));
  }

  removerItem(produtoId: string): void {
    if (this._status !== PedidoStatus.PENDENTE) {
      throw new PedidoNaoPermiteAlteracaoError(this.id, this._status);
    }

    const index = this._itens.findIndex(i => i.produtoId === produtoId);
    if (index === -1) {
      throw new ItemNaoEncontradoNoPedidoError(this.id, produtoId);
    }

    const [removido] = this._itens.splice(index, 1);
    this.atualizarTimestamp();
    this.addEvento(new ItemRemovidoDoPedidoEvent(this.id, removido));
  }

  definirEnderecoEntrega(endereco: EnderecoEntrega): void {
    if (this._status !== PedidoStatus.PENDENTE) {
      throw new PedidoNaoPermiteAlteracaoError(this.id, this._status);
    }

    this._enderecoEntrega = endereco;
    this.atualizarTimestamp();
  }

  confirmar(): void {
    // Invariante: só pode confirmar se pendente
    if (this._status !== PedidoStatus.PENDENTE) {
      throw new TransicaoStatusInvalidaError(
        this.id,
        this._status,
        PedidoStatus.CONFIRMADO
      );
    }

    // Invariante: precisa ter endereço
    if (!this._enderecoEntrega) {
      throw new PedidoSemEnderecoError(this.id);
    }

    // Invariante: precisa ter itens
    if (this._itens.length === 0) {
      throw new PedidoSemItensError(this.id);
    }

    this._status = PedidoStatus.CONFIRMADO;
    this.atualizarTimestamp();
    this.addEvento(new PedidoConfirmadoEvent(this));
  }

  pagar(transacaoId: string): void {
    if (this._status !== PedidoStatus.CONFIRMADO) {
      throw new TransicaoStatusInvalidaError(
        this.id,
        this._status,
        PedidoStatus.PAGO
      );
    }

    this._status = PedidoStatus.PAGO;
    this.atualizarTimestamp();
    this.addEvento(new PedidoPagoEvent(this.id, transacaoId));
  }

  cancelar(motivo: string): void {
    const statusCancelaveis = [
      PedidoStatus.PENDENTE,
      PedidoStatus.CONFIRMADO
    ];

    if (!statusCancelaveis.includes(this._status)) {
      throw new PedidoNaoPodeCancelarError(this.id, this._status);
    }

    this._status = PedidoStatus.CANCELADO;
    this.atualizarTimestamp();
    this.addEvento(new PedidoCanceladoEvent(this.id, motivo));
  }

  // Cálculos de domínio
  calcularTotal(): Money {
    return this._itens.reduce(
      (total, item) => total.add(item.calcularSubtotal()),
      Money.zero('BRL')
    );
  }

  calcularQuantidadeItens(): number {
    return this._itens.reduce((total, item) => total + item.quantidade, 0);
  }

  // Getters (encapsulamento)
  get itens(): readonly ItemPedido[] {
    return Object.freeze([...this._itens]);
  }

  get status(): PedidoStatus {
    return this._status;
  }

  get enderecoEntrega(): EnderecoEntrega | null {
    return this._enderecoEntrega;
  }

  get atualizadoEm(): Date {
    return this._atualizadoEm;
  }

  // Eventos de domínio
  obterEventos(): readonly DomainEvent[] {
    return Object.freeze([...this.eventos]);
  }

  limparEventos(): void {
    this.eventos = [];
  }

  private addEvento(evento: DomainEvent): void {
    this.eventos.push(evento);
  }

  private atualizarTimestamp(): void {
    this._atualizadoEm = new Date();
  }
}

18.4 Persistência (A Memória)

A camada de persistência é a memória do holon. Ela guarda três tipos de dados:

Dados Próprios (Autoridade)

// infrastructure/database/pedido.repository.ts
@Injectable()
export class PedidoRepositoryPrisma implements PedidoRepositoryPort {
  constructor(private readonly prisma: PrismaService) {}

  async salvar(pedido: Pedido): Promise<void> {
    await this.prisma.pedido.upsert({
      where: { id: pedido.id },
      create: {
        id: pedido.id,
        clienteId: pedido.clienteId,
        status: pedido.status,
        enderecoEntrega: pedido.enderecoEntrega
          ? JSON.stringify(pedido.enderecoEntrega)
          : null,
        criadoEm: pedido.criadoEm,
        atualizadoEm: pedido.atualizadoEm,
        itens: {
          create: pedido.itens.map(item => ({
            id: item.id,
            produtoId: item.produtoId,
            nome: item.nome,
            quantidade: item.quantidade,
            precoUnitario: item.precoUnitario.valor,
            moeda: item.precoUnitario.moeda
          }))
        }
      },
      update: {
        status: pedido.status,
        enderecoEntrega: pedido.enderecoEntrega
          ? JSON.stringify(pedido.enderecoEntrega)
          : null,
        atualizadoEm: pedido.atualizadoEm,
        itens: {
          deleteMany: {},
          create: pedido.itens.map(item => ({
            id: item.id,
            produtoId: item.produtoId,
            nome: item.nome,
            quantidade: item.quantidade,
            precoUnitario: item.precoUnitario.valor,
            moeda: item.precoUnitario.moeda
          }))
        }
      }
    });
  }

  async buscarPorId(id: string): Promise<Pedido | null> {
    const dados = await this.prisma.pedido.findUnique({
      where: { id },
      include: { itens: true }
    });

    if (!dados) return null;

    return Pedido.reconstituir({
      id: dados.id,
      clienteId: dados.clienteId,
      status: dados.status as PedidoStatus,
      itens: dados.itens.map(i => ItemPedido.reconstituir({
        id: i.id,
        produtoId: i.produtoId,
        nome: i.nome,
        quantidade: i.quantidade,
        precoUnitario: Money.of(i.precoUnitario, i.moeda)
      })),
      enderecoEntrega: dados.enderecoEntrega
        ? JSON.parse(dados.enderecoEntrega)
        : null,
      criadoEm: dados.criadoEm,
      atualizadoEm: dados.atualizadoEm
    });
  }

  async listarPorCliente(
    clienteId: string,
    opcoes: PaginacaoOpcoes
  ): Promise<PaginatedResult<Pedido>> {
    const [dados, total] = await Promise.all([
      this.prisma.pedido.findMany({
        where: { clienteId },
        include: { itens: true },
        skip: opcoes.offset,
        take: opcoes.limit,
        orderBy: { criadoEm: 'desc' }
      }),
      this.prisma.pedido.count({ where: { clienteId } })
    ]);

    return {
      items: dados.map(d => this.toDomain(d)),
      total,
      offset: opcoes.offset,
      limit: opcoes.limit
    };
  }
}

18.5 Auto-Regulação (O Sistema Imune)

O sistema imune do holon protege contra falhas e mantém a saúde do sistema.

Health Checks

// infrastructure/health/pedido-health.indicator.ts
@Injectable()
export class PedidoHealthIndicator extends HealthIndicator {
  constructor(
    private readonly prisma: PrismaService,
    private readonly kafka: KafkaService,
    private readonly redis: RedisService
  ) {
    super();
  }

  async isHealthy(): Promise<HealthIndicatorResult> {
    const checks = await Promise.allSettled([
      this.checkDatabase(),
      this.checkKafka(),
      this.checkRedis()
    ]);

    const results: Record<string, any> = {};
    let isHealthy = true;

    for (const [index, check] of checks.entries()) {
      const name = ['database', 'kafka', 'redis'][index];
      if (check.status === 'fulfilled') {
        results[name] = check.value;
      } else {
        results[name] = { status: 'down', error: check.reason.message };
        isHealthy = false;
      }
    }

    return this.getStatus('holon-pedidos', isHealthy, results);
  }

  private async checkDatabase(): Promise<{ status: string; latency: number }> {
    const start = Date.now();
    await this.prisma.$queryRaw`SELECT 1`;
    return { status: 'up', latency: Date.now() - start };
  }

  private async checkKafka(): Promise<{ status: string }> {
    const isConnected = await this.kafka.isConnected();
    return { status: isConnected ? 'up' : 'down' };
  }

  private async checkRedis(): Promise<{ status: string; latency: number }> {
    const start = Date.now();
    await this.redis.ping();
    return { status: 'up', latency: Date.now() - start };
  }
}

Circuit Breakers

// infrastructure/resilience/circuit-breaker.decorator.ts
import CircuitBreaker from 'opossum';

export function WithCircuitBreaker(options: CircuitBreakerOptions = {}) {
  const defaultOptions = {
    timeout: 3000,           // Timeout de 3 segundos
    errorThresholdPercentage: 50,  // Abre com 50% de erros
    resetTimeout: 30000,     // Tenta fechar após 30 segundos
    volumeThreshold: 5       // Mínimo de 5 requisições para calcular
  };

  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    const breaker = new CircuitBreaker(
      async (...args: any[]) => originalMethod.apply(target, args),
      { ...defaultOptions, ...options }
    );

    breaker.on('open', () => {
      logger.warn(`Circuit breaker OPEN para ${propertyKey}`);
      metrics.increment('circuit_breaker.open', { method: propertyKey });
    });

    breaker.on('halfOpen', () => {
      logger.info(`Circuit breaker HALF-OPEN para ${propertyKey}`);
    });

    breaker.on('close', () => {
      logger.info(`Circuit breaker CLOSED para ${propertyKey}`);
      metrics.increment('circuit_breaker.close', { method: propertyKey });
    });

    descriptor.value = async function (...args: any[]) {
      return breaker.fire(...args);
    };

    return descriptor;
  };
}

// Uso
@Injectable()
export class GatewayPagamento {
  @WithCircuitBreaker({ timeout: 5000 })
  async processarPagamento(dados: DadosPagamento): Promise<ResultadoPagamento> {
    // Chamada externa que pode falhar
    return this.httpClient.post('/pagamentos', dados);
  }
}

Fallbacks

// application/services/produto-service.ts
@Injectable()
export class ProdutoService {
  constructor(
    private readonly produtoReplica: ProdutoReplicaRepository,
    private readonly cache: CacheService,
    private readonly logger: Logger
  ) {}

  async buscarProduto(produtoId: string): Promise<Produto> {
    // Nível 1: Cache
    const cached = await this.cache.get<Produto>(`produto:${produtoId}`);
    if (cached) {
      return cached;
    }

    // Nível 2: Réplica local
    try {
      const replica = await this.produtoReplica.buscarPorId(produtoId);
      if (replica) {
        await this.cache.set(`produto:${produtoId}`, replica, { ttl: 300 });
        return replica;
      }
    } catch (error) {
      this.logger.warn(`Falha ao buscar réplica do produto ${produtoId}`, error);
    }

    // Nível 3: Fallback com dados mínimos
    return this.criarProdutoFallback(produtoId);
  }

  private criarProdutoFallback(produtoId: string): Produto {
    this.logger.warn(`Usando fallback para produto ${produtoId}`);

    return {
      id: produtoId,
      nome: 'Produto Indisponível',
      preco: Money.zero('BRL'),
      disponivel: false,
      isFallback: true
    };
  }
}