"Conhece-te a ti mesmo." — Inscrição no Templo de Delfos
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) │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
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.
// 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);
}
}// 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()
};
}
}// 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();
}
}
}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.
// 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();
}
}A camada de persistência é a memória do holon. Ela guarda três tipos de dados:
// 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
};
}
}O sistema imune do holon protege contra falhas e mantém a saúde do sistema.
// 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 };
}
}// 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);
}
}// 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
};
}
}