System Design: sistemas de delivery usando arquitetura cell-based

Adriano Croco
6 min readNov 11, 2024

--

Olá!

Hoje gostaria de elaborar com vocês uma arquitetura de um sistema de delivery de alto volume, utilizando uma técnica chamada Cell-Based-Architecture.

Espero que esse texto esclareça que fazer um sistema simples, tipo “ifood”, que já ouvi por aí em briefings para freelancers, não é um trabalho tão simples assim.

Escopo

Pedi para o Claude gerar os requisitos básicos de um serviço qualquer de delivery:

1. O sistema deve permitir que os parceiros cadastrem seus produtos com fotos, preços e descrições detalhadas dos mesmos.
2. Usuários devem poder fazer pedidos escolhendo itens.
3. O sistema precisa calcular automaticamente o valor do frete baseado na distância e mostrar o tempo estimado de entrega.
4. Parceiros devem receber notificações em tempo real dos pedidos e poder atualizar o status (aceito, em preparo, saiu para entrega).
5. Clientes precisam acompanhar em tempo real o status do pedido e poder avaliar tanto o parceiro quanto o entregador após a entrega.

Parece razoável, certo? Leva em questão somente o lado do cliente e do parceiro, sem detalhar pontos importantes como a experiência dos usuários internos e uma estrutura para tomar decisões data-driven, o que é essencial para esse tipo de negócio hoje em dia.

Como o intuito desse texto é ser didático, tentei focar no mínimo necessário em termos sistêmicos para fazer uma operação dessa funcionar. Se você trabalha em alguma operação similar, me perdoe por soar simplista, o intuito aqui é outro, ok?

Considerando todos os requisitos, temos uma estrutura similar a essa, considerando os dominios relativamente separados:

Visão geral dos sistemas de uma empresa de delivery

Só para adicionar variabilidade na arquitetura, alguns serviços usam um único banco de dados isolado, outros usam filas e consumidores para processamento assíncrono e o de pagamentos usa integração com serviços externos. Esses detalhes serão importantes mais adiante.

Portanto, nesse desenho, temos um app que se comunica com um back-end e pode escalar para alguns milhares de usuários se for feito com um certo carinho e não tiver problemas óbvios de performance.

O problema é escalar essa estrutura de forma eficiente. O que me leva ao ponto principal do texto.

Vamos falar de células

Imagine agora que o seu app deu certo. De repente, você tem que lidar com milhões de clientes simultâneos, centenas de milhares de parceiros e dezenas de milhares de entregadores.

Uma técnica muito útil para lidar com sistemas de alto volume (e relativamente pouco falada) é o conceito de arquitetura baseada em células (Cell-Based).

Uma breve definição do termo:

A cell-based architecture organiza o software em unidades independentes chamadas células, cada uma com responsabilidades específicas e capacidade de se comunicar com outras. Cada célula opera de forma autônoma, facilitando a escalabilidade e a modularidade do sistema. Esse modelo é comumente usado em sistemas distribuídos para melhorar a resiliência e a flexibilidade.

Em linhas gerais, é a aplicação de técnicas de modularização em sistemas distribuídos, junto com conceitos de isolamento de falhas como Bulkheads.

Acho que vale comentar um pouco sobre essa técnica em específico.

Bulkheads

O termo vem de um compartimento em navios que permite que, em caso de rachaduras no casco, uma determinada parte da embarcação encha de água, mas impede que o volume de água vá para outros lugares, impedindo que a embarcação afunde. Visualmente, temos:

Exemplo de um bulkhead no contexto naval

Aplicando a mesma ideia para sistemas distribuídos, como podemos isolar falhas de uma forma que os componentes envolvidos não sejam prejudicados de forma global por falhas locais?

A resposta é a mesma: isolando as falhas de alguma forma. Podemos fazer isso com containers, isolamento de recursos de infra, controle de acessos em rede, filas ou até mesmo, isolamento na camada HTTP das aplicações (via código), como por exemplo:

Exemplo de um bulkhead sendo usado em sistemas para isolar falhas no componente C do restante da malha

Esse material aqui tem mais detalhes sobre.

Aplicando isolamento a arquitetura como um todo

Agora, vamos expandir um pouco a ideia. Imagine que um bloco único e indivisível de um determinado sistema consegue processar, de forma isolada, todas as requisições de ponta a ponta de um determinado cliente, podendo ser escalado separadamente dos demais.

Pareceu bom demais para ser verdade? Nem tanto. É possível, só que é um pouquinho caro. Adaptando o mesmo desenho anterior, temos o seguinte:

Células isoladas (com agrupamento por modo de operação)

Eu agrupei serviços com o mesmo modo de operação para fins de brevidade e para o desenho ficar legível. Mas, o desenho correto final e completo é esse aqui:

Desenho geral do ecossistema (sem agrupamento)

Parece complexo? é porque é. Caro? Também. Os custos relacionados a recursos como redundância de banco de dados, trafégo de rede e demais pontos relacionados a nuvem sobem consideravelmente.

Portanto, a recomendação para essa técnica funcionar bem é usar essa separação por chunks para quantias relativamente grandes de usuários (por milhão ou algo similar).

Além disso, mais algumas adaptações teriam que ser feitas:

Primeiro, uma determinada requisição/usuário teria de ser processada idealmente por uma única célula. Para conseguir fazer isso, teríamos que isolar os bancos de dados (com técnicas como sharding, que já abordei aqui) e correlation_ids. Além disso, teriamos que pensar em um mecanismo de routing, ou seja, que garante que cada user_id especificamente seja processado em uma determinada célula.

O motivo disso é simples: imagine que o banco do serviço de pagamentos, por exemplo, tem essa separação de usuários por sharding. Como o serviço/banco saberia em qual shard os dados do usuário estão?

Isso pode ser feito com um componente principal de roteamento que, para cada request, traduz o user_id para a célula correspondente ou adaptando cada serviço para que saibam como achar em qual shart está cada user_id.

O trade-off neste caso é similar ao que existe ao utilizar Sagas: você garante centralidade e consistência em troca de um ponto único de falha (Orquestração) ou distribui tudo, ganha resiliência, mais espalha complexidade ao mesmo tempo (Coreografia).

Acredito que aqui resida a maior complexidade desse tipo de design. Sem um mecanismo confiável de roteamento de usuários no ecossistema, essa arquitetura pode se tornar um desafio na hora de encontrar logs do que aconteceu com cada usuário, por exemplo.

Fora isso, imagine o impacto que aconteceria ao ecossistema se um determinado usuário fosse processado em outra célula? Vários problemas de integridade poderiam advir disso.

Como tudo em arquitetura, tenha muito cuidado ao utilizar técnicas complexas dessa natureza. Lembre-se do corolário da primeira regra da arquitetura do Neal Ford: se você não acha que tem um trade-off em algum lugar, é porque você só não achou ele ainda.

Um exemplo da vida real que encontrei que usa ideias similares foi esse, apesar de mencionar bastante a camada de infra e chamar o que eu chamo de células de shards, a ideia geral é a mesma.

Espero que este texto tenha sido útil de alguma forma.

Até!

Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.

--

--

Adriano Croco
Adriano Croco

No responses yet