Quando sua aplicação depende de APIs externas — provedores de pagamento, marketplaces, serviços de envio — você está aceitando que parte do seu sistema está fora do seu controle. Essas dependências podem ficar lentas, retornar erros, ou simplesmente parar de responder. Sem proteção, uma API externa instável pode derrubar todo o seu sistema. O Circuit Breaker é o padrão que evita isso.
O problema: falhas em cascata
Imagine um cenário real: sua aplicação faz uma chamada para a API de um marketplace durante o checkout. Normalmente essa chamada leva 200ms. Mas o marketplace está com problemas e começa a demorar 30 segundos para responder — ou simplesmente não responde.
O que acontece sem proteção:
- O checkout do usuário trava esperando a resposta do marketplace
- Enquanto espera, novas requisições chegam e também ficam travadas
- O pool de conexões do seu servidor esgota
- Seu servidor para de responder para todos os usuários — não só os que dependem do marketplace
- O sistema inteiro cai por causa de uma dependência externa
Isso é uma falha em cascata. Uma dependência instável propaga a falha para todo o sistema. O Circuit Breaker interrompe essa propagação.
Como funciona o Circuit Breaker
O padrão funciona como um disjuntor elétrico. Ele monitora as chamadas para uma dependência externa e tem três estados:
- CLOSED (fechado) — estado normal. Requisições passam para a API externa. Se falhas acumulam além do threshold, transiciona para OPEN.
- OPEN (aberto) — estado de proteção. Requisições não vão para a API. Retorna fallback imediatamente. Após um período de cooldown, transiciona para HALF-OPEN.
- HALF-OPEN (meio-aberto) — estado de teste. Permite uma requisição passar como teste. Se sucesso, volta para CLOSED. Se falha, volta para OPEN.
Diagrama da state machine do Circuit Breaker — estados CLOSED, OPEN e HALF-OPEN com transiçõesImplementação em TypeScript
Vou implementar um Circuit Breaker genérico que pode envolver qualquer chamada externa. A ideia é que ele seja reutilizável para diferentes dependências.
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'
interface CircuitBreakerOptions {
failureThreshold: number // falhas antes de abrir
cooldownMs: number // tempo em OPEN antes de testar
timeoutMs: number // timeout por requisição
}
class CircuitBreaker {
private state: CircuitState = 'CLOSED'
private failureCount = 0
private lastFailureTime = 0
private readonly options: CircuitBreakerOptions
constructor(options: Partial<CircuitBreakerOptions> = {}) {
this.options = {
failureThreshold: options.failureThreshold ?? 5,
cooldownMs: options.cooldownMs ?? 60_000,
timeoutMs: options.timeoutMs ?? 10_000,
}
}
async execute<T>(fn: () => Promise<T>, fallback: () => T): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime >= this.options.cooldownMs) {
this.state = 'HALF_OPEN'
} else {
return fallback()
}
}
try {
const result = await this.withTimeout(fn())
this.onSuccess()
return result
} catch (error) {
this.onFailure()
return fallback()
}
}
private onSuccess() {
this.failureCount = 0
this.state = 'CLOSED'
}
private onFailure() {
this.failureCount++
this.lastFailureTime = Date.now()
if (this.failureCount >= this.options.failureThreshold) {
this.state = 'OPEN'
}
}
private withTimeout<T>(promise: Promise<T>): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), this.options.timeoutMs)
),
])
}
getState(): CircuitState {
return this.state
}
}Uso prático: protegendo uma chamada de API
Com a classe criada, envolver qualquer chamada externa é simples:
// Um circuit breaker por dependência
const marketplaceBreaker = new CircuitBreaker({
failureThreshold: 3, // abre após 3 falhas
cooldownMs: 30_000, // espera 30s antes de testar
timeoutMs: 5_000, // timeout de 5s por chamada
})
const paymentBreaker = new CircuitBreaker({
failureThreshold: 2, // mais sensível — é pagamento
cooldownMs: 60_000, // cooldown maior
timeoutMs: 10_000, // timeout maior — provedores de pagamento são lentos
})
// Chamada protegida
async function getProductFromMarketplace(productId: string) {
return marketplaceBreaker.execute(
// Chamada real
() => fetch(`https://api.marketplace.com/products/${productId}`)
.then(res => res.json()),
// Fallback quando o circuit está aberto
() => getCachedProduct(productId)
)
}O ponto importante: cada dependência externa deve ter seu próprio Circuit Breaker. Se o marketplace cai, o circuit do marketplace abre — mas o circuit do provedor de pagamento continua fechado e funcionando normalmente.
Estratégias de fallback
O fallback é o que o seu sistema retorna quando o circuit está aberto. Essa é a parte que exige mais decisão de produto, porque o fallback precisa ser útil o suficiente para o usuário não perceber a degradação. Algumas estratégias:
- Cache — retornar a última resposta válida. Funciona bem para dados que mudam pouco (catálogo de produtos, configurações).
- Valor padrão — retornar um valor seguro pré-definido. Funciona para cálculos de frete ("frete a confirmar") ou estoque ("consulte disponibilidade").
- Funcionalidade reduzida — o checkout funciona sem a validação do marketplace, mas avisa o usuário que o preço será confirmado depois.
- Fila para retry posterior — salvar a operação para tentar novamente quando o serviço voltar. Funciona para operações que não precisam de resposta imediata.
// Fallback com cache
const productCache = new Map<string, Product>()
async function getProduct(id: string): Promise<Product> {
return marketplaceBreaker.execute(
async () => {
const product = await fetchFromMarketplace(id)
productCache.set(id, product) // atualiza cache em caso de sucesso
return product
},
() => {
const cached = productCache.get(id)
if (cached) return cached
// Sem cache — retorna produto com flag de indisponibilidade
return { id, name: 'Produto indisponível', available: false }
}
)
}Combinando com retry
Circuit Breaker e retry resolvem problemas diferentes:
- Retry — para falhas pontuais (um request falhou, o próximo provavelmente funciona). Usa backoff exponencial.
- Circuit Breaker — para falhas sustentadas (o serviço está fora, não adianta ficar tentando). Protege o sistema inteiro.
A combinação correta: retry dentro do circuit breaker. O circuit monitora se os retries estão funcionando. Se mesmo com retry a chamada continua falhando, o circuit abre.
async function fetchWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error | null = null
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
// Backoff exponencial: 1s, 2s, 4s
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000))
}
}
throw lastError
}
// Retry dentro do circuit breaker
async function getProductReliably(id: string) {
return marketplaceBreaker.execute(
() => fetchWithRetry(() => fetchFromMarketplace(id), 3),
() => getCachedProduct(id)
)
}Monitoramento: sabendo quando o circuit abre
Um circuit breaker que abre silenciosamente é perigoso — você precisa saber quando um serviço externo está instável. Adicionar eventos de transição de estado resolve isso:
class ObservableCircuitBreaker extends CircuitBreaker {
private name: string
constructor(name: string, options?: Partial<CircuitBreakerOptions>) {
super(options)
this.name = name
}
async execute<T>(fn: () => Promise<T>, fallback: () => T): Promise<T> {
const previousState = this.getState()
const result = await super.execute(fn, fallback)
const currentState = this.getState()
if (previousState !== currentState) {
this.logTransition(previousState, currentState)
}
return result
}
private logTransition(from: string, to: string) {
const message = `[CircuitBreaker:${this.name}] ${from} → ${to}`
if (to === 'OPEN') {
console.error(message) // alertar quando abre
// Enviar para monitoring (Datadog, Sentry, etc.)
} else if (to === 'CLOSED') {
console.info(message) // informar quando recupera
} else {
console.warn(message) // half-open é transição
}
}
}Com isso, você sabe exatamente quando um serviço externo começa a falhar e quando se recupera. Em produção, esses logs devem ir para um sistema de alertas — se o circuit do provedor de pagamento abre, alguém precisa ser notificado.
Cenário real: API de marketplace inconsistente
Um cenário que enfrentei: uma API de marketplace retornava dados de produtos de forma inconsistente. Às vezes funcionava em 200ms, às vezes demorava 15 segundos, às vezes retornava 500. O checkout dependia dessa API para validar preço e estoque.
A solução foi:
- Circuit Breaker com threshold de 3 falhas e cooldown de 30s
- Timeout agressivo de 3s (se não respondeu em 3s, considera falha)
- Cache como fallback — última resposta válida do produto, com flag de "preço sujeito a confirmação"
- Reconciliação posterior — job que roda a cada 5 minutos verificando se os preços do cache ainda estão corretos
O resultado: o checkout nunca mais travou por causa do marketplace. Quando a API estava instável, os usuários viam os preços do cache com um aviso sutil. Quando voltava ao normal, o circuit fechava e tudo voltava a funcionar em tempo real.
Quando não usar Circuit Breaker
Nem toda chamada externa precisa de circuit breaker:
- Chamadas que já são resilientes — se a dependência tem retry nativo e SLA alto (ex: AWS S3), o overhead do circuit pode ser desnecessário.
- Operações idempotentes com fila — se você já usa uma fila com retry (ex: webhook processing), a fila já faz o papel do circuit.
- Chamadas que não são críticas — analytics, logging externo, tracking. Se falhar, não afeta o usuário.
Resumo
O Circuit Breaker é uma das ferramentas mais importantes para aplicações que dependem de serviços externos. O padrão em si é simples — uma state machine com três estados. O que exige engenharia é:
- Calibrar os thresholds — quantas falhas antes de abrir, quanto tempo de cooldown
- Projetar fallbacks úteis — cache, valor padrão, funcionalidade reduzida
- Monitorar transições — saber quando abriu e quando fechou
- Um breaker por dependência — isolar falhas para que uma API instável não afete as outras
- Combinar com retry — retry para falhas pontuais, circuit para falhas sustentadas
A diferença entre um sistema que cai junto com suas dependências e um que degrada graciosamente é quase sempre um Circuit Breaker bem configurado.
