Luiz Schons
Projetar sistemas assumindo que falhas vão acontecer, não tentando evitá-las.
Sim, e por diversos motivos - software, hardware, rede ou fatores externos.
Devemos construir sistemas resilientes que continuem funcionando mesmo quando falhas ocorrem.
Você tem uma ideia brilhante para um e-commerce que vai mudar o mundo.
Quem nunca começou assim? 😅
Parece até datacenter, não é mesmo? 🤣
Você ainda tem poucos usuários, então tudo funciona bem.
Com o aumento de usuários, seu servidor local começa a mostrar sinais de sobrecarga.
É hora de repensar a infraestrutura: escalar para suportar mais usuários e garantir a continuidade do serviço quando falhas ocorrerem.
Investimento em um servidor dedicado com mais recursos computacionais.
Mas você esqueceu de um "pequeno" detalhe: servidores precisam de manutenção constante.
E você não tem uma equipe de infraestrutura...
Ah, e no tempo livre... desenvolver o seu e-commerce! 😰
Meu negócio pode suportar esse risco?
Um único servidor não é suficiente...
Precisamos de uma estratégia melhor!
O próximo passo: múltiplos servidores para eliminar pontos únicos de falha e garantir que seu e-commerce esteja sempre disponível.
Infraestrutura robusta com servidor principal, backup e banco de dados separado — tudo configurado para continuar funcionando mesmo quando um componente falha.
Sua sala não foi projetada para ser um datacenter profissional...
O barato sai caro... muito caro!
Uma solução que:
Alguém tem alguma ideia?
Quais opções temos hoje para:
Migração da infraestrutura para provedores especializados em nuvem que oferecem:
Fernanda Kipper no YouTube
Foco no negócio, não na infraestrutura
O caminho para a resiliência está apenas começando...
Mas sim de como projetar sistemas que sobrevivam quando as coisas dão errado
Porque, acredite, elas sempre dão! 😅
Pense em todos os pontos de falha possíveis...
E como podemos projetar nossas aplicações para lidar com eles
Tente novamente após uma falha, aumentando gradualmente o tempo de espera entre tentativas:
Os padrões de resiliência funcionam em qualquer linguagem de programação
Vamos ver exemplos em duas plataformas diferentes:
Mesmo problema, mesmos conceitos, diferentes implementações
namespace App\Service;
use Hyperf\Retry\Annotation\Retry;
use Hyperf\Retry\RetryBudget;
class PaymentService
{
#[Retry(delay: 1000, maxAttempts: 3)]
public function processPayment()
{
// make a remote call
}
}
import { Injectable } from '@nestjs/common';
import { Retry } from '@nestjs/axios';
import { catchError, delay, retryWhen } from 'rxjs/operators';
import { of } from 'rxjs';
@Injectable()
export class PaymentService {
@Retry({ delay: 1000, maxAttempts: 3 })
async processPayment() {
// make a remote call
}
}
E se o serviço estiver completamente fora do ar?
Vamos ficar tentando sem parar?
Interrompe chamadas a serviços com falhas, prevenindo sobrecarga do sistema
Inspirado nos disjuntores elétricos:
Funcionam melhor quando usados em conjunto!
namespace App\Services;
use App\Gateway\GatewayServiceClient;
use Hyperf\CircuitBreaker\Annotation\CircuitBreaker;
use Hyperf\Di\Annotation\Inject;
class GatewayService
{
#[Inject]
private GatewayServiceClient $client;
#[CircuitBreaker
(
options: ['timeout' => 5000],
failCounter: 3,
successCounter: 1,
fallback: 'processPaymentFallback'
)]
public function processPayment()
{
// make a remote call
}
public function processPaymentFallback()
{
return 'Service unavailable';
}
}
import { Injectable } from '@nestjs/common';
import { CircuitBreaker } from '@nestjs/axios';
@Injectable()
export class PaymentService {
@CircuitBreaker({
timeout: 5000,
fallback: () => 'Service unavailable'
})
async processPayment() {
// make a remote call
}
}
Algo comum nas duas implementações...
É como levar um guarda-chuva mesmo quando o céu está limpo 🌂
namespace App\Services;
use App\Gateway\GatewayServiceClient;
use Hyperf\CircuitBreaker\Annotation\CircuitBreaker;
use Hyperf\Di\Annotation\Inject;
class GatewayService
{
#[Inject]
private GatewayServiceClient $client;
#[CircuitBreaker
(
options: ['timeout' => 5000],
failCounter: 3,
successCounter: 1,
fallback: 'processPaymentFallback' // Nome do método de fallback
)]
public function processPayment()
{
// make a remote call
}
// Método executado quando um pedido é cancelado
public function processPaymentFallback()
{
return 'Service unavailable';
}
}
import { Injectable } from '@nestjs/common';
import { CircuitBreaker } from '@nestjs/axios';
@Injectable()
export class PaymentService {
@CircuitBreaker({
timeout: 5000,
fallback: () => 'Service unavailable' // Função inline de fallback
})
async processPayment() {
// make a remote call
}
}
Gerencia transações distribuídas em sistemas complexos
Transações robustas com recuperação em cada etapa
Quando algo falha, precisamos desfazer as operações anteriores
orderId;
// Compensa as etapas anteriores (saga pattern)
$this->refundPayment($orderId);
$this->returnInventory($orderId);
$this->notifyCustomer($orderId, 'Seu pedido foi cancelado');
$this->logCancellation($orderId, $event->reason);
}
}
import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { PaymentService } from '../services/payment.service';
import { InventoryService } from '../services/inventory.service';
import { NotificationService } from '../services/notification.service';
import { LoggingService } from '../services/logging.service';
@Injectable()
export class OrderCancelledListener {
constructor(
private paymentService: PaymentService,
private inventoryService: InventoryService,
private notificationService: NotificationService,
private loggingService: LoggingService,
) {}
@RabbitSubscribe({
exchange: 'order_exchange',
routingKey: 'order.cancelled',
queue: 'order-cancelled-queue',
})
async handleCancellation(msg: { orderId: number; reason: string }) {
// Compensação para cada etapa anterior (saga pattern)
await this.paymentService.refund(msg.orderId);
await this.inventoryService.restoreStock(msg.orderId);
await this.notificationService.notify(msg.orderId, 'Seu pedido foi cancelado');
await this.loggingService.logCancellation(msg.orderId, msg.reason);
}
}
Cada ação tem sua compensação correspondente
Como lidar com esse tipo de problema?
Uma estratégia para lidar com serviços externos indisponíveis
namespace App\Services;
use App\Shipping\ShippingServiceClient;
use Hyperf\CircuitBreaker\Annotation\CircuitBreaker;
use Hyperf\Di\Annotation\Inject;
class GatewayService
{
#[Inject]
private ShippingServiceClient $client;
#[CircuitBreaker
(
options: ['timeout' => 5000],
failCounter: 3,
successCounter: 1,
fallback: 'processCalculateShippingFallback'
)]
public function processCalculateShipping($order)
{
// Tenta calcular o frete usando a API externa
$shipping = $this->client
->calculateShipping($order);
// Salva o resultado em cache para uso futuro
$this->cacheShipping($order, $shipping);
// Retorna o resultado do cálculo
return $shipping;
}
// Método executado quando o circuito abre
public function processCalculateShippingFallback($order)
{
// Retorna um valor padrão ou cache com base no último cálculo
$cachedShipping = $this->getCachedShipping($order);
if ($cachedShipping) {
return $cachedShipping;
}
// Se não houver cache, retorna um valor padrão
return [
'price' => 40.00, // Valor fixo de frete
'deliveryTime' => '5-7 dias úteis', // Tempo de entrega padrão
];
}
}
Um sistema resiliente combina múltiplas estratégias
Endpoint GET /products responde rapidamente para cada usuário
Recursos do servidor são suficientes para atender a todos
O servidor fica sobrecarregado e ninguém consegue usar o sistema
Limita o número de requisições que um cliente pode fazer em um período de tempo
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { RateLimiterRedis } from 'rate-limiter-flexible';
@Injectable()
export class RateLimiterMiddleware implements NestMiddleware {
private rateLimiter: RateLimiterRedis;
constructor() {
// Configuração do rate limiter
this.rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 60, // Número de requisições permitidas
duration: 60, // Por minuto
});
}
async use(req: Request, res: Response, next: NextFunction) {
try {
// Usa o IP como identificador
const key = req.ip;
await this.rateLimiter.consume(key);
next();
} catch (error) {
// Limite excedido
const retryAfter = Math.round(error.msBeforeNext / 1000) || 1;
res.set('Retry-After', String(retryAfter));
res.status(429).json({
error: 'Muitas requisições, tente novamente mais tarde',
retryAfter
});
}
}
}
Equilibra as cargas para um serviço mais justo
Como expandir nosso sistema sem quebrar o que já funciona?
Princípio Aberto/Fechado do SOLID
"Entidades de software devem estar abertas para extensão, mas fechadas para modificação."
Em outras palavras: adicione comportamentos sem mexer no código existente
E se quisermos adicionar novos provedores além dos Correios?
// Interface comum para todos os provedores de frete
interface ShippingProviderInterface
{
/**
* Calcula o frete para um pedido
*/
public function calculateShipping(array $order): array;
public function trackShipment(string $trackingCode): array;
public function getName(): string;
}
// Implementação para os Correios (já existente)
class CorreiosShippingProvider implements ShippingProviderInterface
{
public function calculateShipping(array $order): array
{
// Lógica específica dos Correios
return [
'price' => 35.90,
'delivery_time' => '3-5 dias úteis',
'provider' => 'Correios'
];
}
// Outras implementações de métodos...
}
// Nova implementação para transportadora sem modificar código existente
class FastShippingProvider implements ShippingProviderInterface
{
public function calculateShipping(array $order): array
{
// Lógica específica da transportadora privada
return [
'price' => 45.90,
'delivery_time' => '1-2 dias úteis',
'provider' => 'FastShipping'
];
}
// Outras implementações de métodos...
}
// Factory que seleciona o provedor apropriado
class ShippingProviderFactory
{
private array $providers = [];
public function registerProvider(ShippingProviderInterface $provider): void
{
$this->providers[$provider->getName()] = $provider;
}
public function getProvider(string $name): ?ShippingProviderInterface
{
return $this->providers[$name] ?? null;
}
public function getAllProviders(): array
{
return $this->providers;
}
}
// Configuração da aplicação
$factory = new ShippingProviderFactory();
// Registramos todos os provedores disponíveis
$factory->registerProvider(new CorreiosShippingProvider());
$factory->registerProvider(new FastShippingProvider());
// Podemos adicionar novos provedores aqui sem modificar o código existente
$factory->registerProvider(new InternationalShippingProvider());
// Cliente escolhe qual provedor usar
$providerName = $_POST['shipping_provider'] ?? 'Correios';
$provider = $factory->getProvider($providerName);
// Calculamos o frete usando o provedor escolhido
if ($provider) {
return $provider->calculateShipping($order);
}
return [
'error' => 'Provedor de frete não encontrado',
'available_providers' => $factory->getAllProviders()
];
Aplicando SOLID, ganhamos resiliência e flexibilidade.
Quais padrões de resiliência vimos hoje?
Tentar novamente após uma falha, aumentando gradualmente o tempo de espera entre tentativas.
Interromper chamadas a serviços com falhas persistentes, prevenindo sobrecarga e permitindo recuperação gradual.
Planos B para quando algo falha, oferecendo alternativas em vez de deixar a operação falhar completamente.
Gerenciar transações distribuídas em sistemas complexos, com compensações para desfazer operações em caso de falha.
Armazenar resultados de operações para uso quando serviços externos estiverem indisponíveis, melhorando disponibilidade.
Limitar o número de requisições que um cliente pode fazer em um período, protegendo contra abusos e distribuindo recursos de forma justa.
Adicionar novos comportamentos sem modificar o código existente, aumentando a flexibilidade e reduzindo riscos.
Projete para falhar!
Luiz Schons
Senior Software Architect