Um sistema de microserviços orientado a eventos organiza a comunicação entre partes independentes do software por meio de mensagens, em vez de chamadas diretas e acopladas. Nesse modelo, um serviço publica um evento (uma informação relevante) e outro serviço consome esse evento quando estiver disponível, o que favorece escalabilidade e tolerância a falhas.
Uma arquitetura comum para esse tipo de solução combina FastAPI (framework web em Python com suporte a operações assíncronas) e Apache Kafka (plataforma de mensageria e streaming de eventos). O Kafka atua como um “canal central” durável onde eventos são gravados em tópicos, e serviços produtores e consumidores interagem por meio desses tópicos. A execução com Docker e Docker Compose padroniza ambiente, rede e dependências.
Visão geral da arquitetura com Kafka e FastAPI
A arquitetura é composta por dois microserviços e um cluster Kafka mínimo para desenvolvimento. O microserviço A recebe requisições HTTP e publica mensagens em um tópico do Kafka, funcionando como produtor. O microserviço B se conecta ao mesmo tópico e lê as mensagens em tempo real, funcionando como consumidor.
O Kafka organiza mensagens em tópicos, que são como “filas” com histórico persistido, permitindo reprocessamento. Cada mensagem é anexada em sequência e pode ser consumida por múltiplos grupos de consumidores. O componente Zookeeper aparece em imagens tradicionais do Kafka para coordenar metadados em setups legados e de desenvolvimento.
Em Docker, todos os containers entram na mesma rede lógica do Compose, o que permite usar nomes de serviço como DNS interno. Isso explica o uso de kafka:9092 nos microserviços, pois “kafka” é o nome do serviço no arquivo docker-compose. A comunicação HTTP externa ocorre via mapeamento de portas para o host.
Estrutura de pastas recomendada para microserviços com Docker
Uma organização simples e consistente ajuda a manter cada microserviço isolado, com seu próprio Dockerfile e dependências. A raiz do projeto concentra o docker-compose.yml e arquivos auxiliares, enquanto cada microserviço fica em uma pasta própria. Essa estrutura reduz acoplamento e facilita o build separado de imagens.
A estrutura abaixo mostra como os arquivos se relacionam e onde ficam os pontos de configuração. O foco é manter cada serviço com uma pasta contendo aplicação, manifesto de dependências e Dockerfile. O Compose, na raiz, orquestra Kafka, Zookeeper e os dois serviços.
A lista a seguir apresenta uma estrutura de diretórios organizada para esse cenário:
- docker-compose.yml
- Makefile
- microservice-a/
- Dockerfile
- pyproject.toml
- uv.lock (opcional, caso exista)
- app/
- main.py
- microservice-b/
- Dockerfile
- pyproject.toml
- uv.lock (opcional, caso exista)
- app/
- main.py
Componentes essenciais: tópico, bootstrap servers e grupos de consumo
O tópico é o “canal” lógico onde eventos são publicados e armazenados. No exemplo, o tópico test-topic é usado pelos dois microserviços, garantindo que o consumidor enxergue exatamente o que o produtor envia. Em Kafka, tópicos podem ter várias partições, o que aumenta paralelismo e throughput.
O parâmetro bootstrap_servers indica como o cliente encontra o cluster Kafka. Em Docker Compose, kafka:9092 aponta para o container “kafka” na rede interna do Compose. Isso é diferente do acesso pelo host, onde uma porta pode ser exposta, mas internamente os containers conversam pelo nome do serviço.
No consumidor, o group_id define um grupo de consumo, que permite distribuir mensagens entre instâncias do mesmo consumidor. Quando há mais de uma instância no mesmo grupo, cada mensagem de uma partição é entregue a apenas uma instância daquele grupo. Isso viabiliza escalabilidade horizontal do microserviço consumidor.
Microserviço A: produtor HTTP com FastAPI publicando no Kafka
O microserviço A expõe um endpoint HTTP que recebe JSON e transforma o conteúdo em uma mensagem Kafka. A biblioteca aiokafka é um cliente Kafka assíncrono para Python, adequado para aplicações com async e await. A ideia central é manter o produtor ativo durante o ciclo de vida do serviço para evitar overhead a cada requisição.
O evento de inicialização (startup) cria o produtor e tenta conectar em loop, o que evita falha imediata quando o Kafka ainda está subindo. O evento de finalização (shutdown) encerra o produtor com segurança, evitando recursos abertos. O endpoint /produce valida a entrada via Pydantic e envia a mensagem para o tópico.
O trecho abaixo mostra uma implementação completa do produtor conforme o conteúdo fornecido:
import asyncio
from fastapi import FastAPI
from pydantic import BaseModel
from aiokafka import AIOKafkaProducer
from aiokafka.errors import KafkaConnectionError
app = FastAPI()
KAFKA_TOPIC = "test-topic"
KAFKA_BOOTSTRAP_SERVERS = "kafka:9092"
producer: AIOKafkaProducer | None = None
class Message(BaseModel):
content: str
@app.on_event("startup")
async def startup_event():
global producer
producer = AIOKafkaProducer(bootstrap_servers=KAFKA_BOOTSTRAP_SERVERS)
while True:
try:
await producer.start()
print("Kafka producer connected")
break
except KafkaConnectionError:
print("Kafka producer not available. Retrying in 5 seconds...")
await asyncio.sleep(5)
@app.on_event("shutdown")
async def shutdown_event():
global producer
if producer:
await producer.stop()
print("Kafka producer stopped")
@app.post("/produce")
async def produce_message(message: Message):
if producer is None:
return {"error": "Kafka producer is not started"}
await producer.send_and_wait(KAFKA_TOPIC, message.content.encode("utf-8"))
return {"status": "message sent", "content": message.content}
Microserviço B: consumidor assíncrono com FastAPI lendo do Kafka
O microserviço B mantém um consumidor Kafka rodando em background e imprime mensagens recebidas. O AIOKafkaConsumer se inscreve em um tópico e entrega mensagens como um fluxo assíncrono. Isso é útil para processamentos contínuos, como auditoria, enriquecimento de dados ou disparo de outras ações.
O consumidor também tenta conectar em loop para lidar com o tempo de subida do Kafka, evitando encerramento prematuro. O parâmetro auto_offset_reset com valor “earliest” indica que, quando não houver offset salvo para o grupo, o consumo começa do início do tópico. Em cenários reais, esse comportamento é escolhido conforme necessidade de reprocessamento.
O evento de startup cria uma tarefa assíncrona com asyncio.create_task, mantendo a API disponível enquanto o consumo ocorre em paralelo. O endpoint / serve como verificação simples de disponibilidade do serviço.
O trecho abaixo mostra uma implementação completa do consumidor conforme o conteúdo fornecido:
from fastapi import FastAPI
import asyncio
from aiokafka import AIOKafkaConsumer
from aiokafka.errors import KafkaConnectionError
app = FastAPI()
KAFKA_TOPIC = "test-topic"
KAFKA_BOOTSTRAP_SERVERS = "kafka:9092"
async def consume():
print("Entering in consume")
consumer = AIOKafkaConsumer(
KAFKA_TOPIC,
bootstrap_servers=KAFKA_BOOTSTRAP_SERVERS,
group_id="my-fastapi-consumer-group",
auto_offset_reset="earliest",
)
while True:
try:
await consumer.start()
print("Kafka consumer connected and listening...")
break
except KafkaConnectionError:
print("Kafka not available. Retrying in 5 seconds...")
await asyncio.sleep(5)
while True:
try:
print("Waiting for messages..")
async for msg in consumer:
print(f"Message received: {msg.value.decode('utf-8')}")
except Exception as e:
print(f"Error in consumer loop: {e}")
await asyncio.sleep(5)
@app.on_event("startup")
async def startup_event():
print("Startup event launched...")
await asyncio.sleep(10)
asyncio.create_task(consume())
@app.get("/")
async def root():
return {"message": "Microservice B is running!"}
Dockerfile por microserviço: imagem Python, dependências e execução
Cada microserviço possui seu próprio Dockerfile, o que permite builds e versões independentes. A imagem base python:3.12-slim reduz tamanho e inclui o mínimo necessário para rodar Python. A variável PYTHONUNBUFFERED=1 força logs em tempo real no console, o que melhora observabilidade em containers.
O processo típico copia primeiro os arquivos de dependências para aproveitar cache de build. Em seguida, instala-se o projeto e, por fim, copia-se o código da aplicação. Isso evita reinstalar dependências a cada alteração de arquivo em desenvolvimento.
Como requisito, a instalação deve ser feita com pip install “normal”, sem uso de uv. O exemplo abaixo assume que o pyproject.toml descreve dependências e que o projeto é instalável, o que pode ser atendido com configuração PEP 517/518 e um backend de build apropriado.
O bloco a seguir apresenta um Dockerfile compatível com essa abordagem para ambos os microserviços:
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Copia manifestos primeiro para melhor cache de build
COPY pyproject.toml ./
COPY uv.lock ./ # opcional, caso exista no repositório
# Instala dependências do projeto via pip
RUN pip install --upgrade pip && pip install .
# Copia o código da aplicação
COPY ./app ./app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Docker Compose: orquestração de Kafka, Zookeeper e microserviços
O docker-compose.yml define todos os serviços e cria uma rede compartilhada automaticamente. Isso permite que “microservice-a” acesse “kafka” pelo hostname kafka, sem precisar de IP fixo. O Compose também controla ordem de subida com depends_on, embora isso não garanta que o Kafka já esteja pronto para aceitar conexões.
O serviço Kafka é configurado com variáveis de ambiente importantes, como listeners e advertised listeners. O KAFKA_ADVERTISED_LISTENERS define como o broker se anuncia para clientes dentro da rede do Compose, por isso usa kafka:9092. O KAFKA_AUTO_CREATE_TOPICS_ENABLE facilita o exemplo ao permitir criar tópico automaticamente quando a primeira mensagem é publicada.
Nos microserviços, o mapeamento de portas expõe o Uvicorn para o host, permitindo testes via HTTP. O command usa --reload para desenvolvimento, recarregando o servidor quando arquivos mudam. Em volumes, o ideal é montar a pasta correta de cada serviço, evitando que um serviço monte o código do outro.
O bloco abaixo apresenta o compose conforme o conteúdo fornecido, seguido de uma versão corrigida do volume do microserviço A para refletir a estrutura esperada:
version: '3.8'
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
kafka:
image: confluentinc/cp-kafka:latest
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
KAFKA_NUM_PARTITIONS: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
depends_on:
- zookeeper
microservice-a:
build: ./microservice-a
depends_on:
- kafka
ports:
- "8001:8000"
volumes:
- ./microservice-b/app:/app/app
command: >
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
restart: on-failure
microservice-b:
build: ./microservice-b
depends_on:
- kafka
ports:
- "8002:8000"
volumes:
- ./microservice-b/app:/app/app
command: >
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
restart: on-failure
version: '3.8'
services:
microservice-a:
build: ./microservice-a
ports:
- "8001:8000"
volumes:
- ./microservice-a/app:/app/app
Makefile: padronização de comandos de ambiente e testes
Um Makefile serve como atalho para comandos repetitivos, reduzindo variações e erros de digitação. Cada alvo (target) encapsula uma ação, como subir containers, derrubar ambiente ou ver logs. Em times, isso melhora consistência do fluxo local e simplifica documentação.
O alvo up faz build e sobe os serviços, enquanto down remove containers e volumes para limpeza total. O alvo test-produce envia uma mensagem HTTP ao produtor com curl, exercitando a cadeia completa até o consumidor. Os alvos logs-a e logs-b exibem logs em tempo real para observar mensagens consumidas.
O bloco abaixo reproduz um Makefile completo conforme o conteúdo fornecido:
# Variables
PROJECT_NAME=fastapi-microservices-kafka
COMPOSE=docker-compose
PY=python3
# Start development environment with hot reload
up:
@echo "Starting development environment..."
$(COMPOSE) up --build
# Stop and remove all containers, volumes, and networks
down:
@echo "Stopping and cleaning up containers and volumes..."
$(COMPOSE) down -v
# Stop containers without removing volumes
stop:
$(COMPOSE) down
# Rebuild images without using cache
rebuild:
$(COMPOSE) build --no-cache
# Send a test message to the Kafka producer
test-produce:
curl -X POST http://localhost:8001/produce \
-H "Content-Type: application/json" \
-d '{"content": "Test message from Makefile"}'
# View logs for the consumer microservice
logs-b:
$(COMPOSE) logs -f microservice-b
# View logs for the producer microservice
logs-a:
$(COMPOSE) logs -f microservice-a
# Clean up unused Docker data
clean:
docker system prune -f
# Open a shell in the producer container
sh-a:
$(COMPOSE) exec microservice-a sh
# Open a shell in the consumer container
sh-b:
$(COMPOSE) exec microservice-b sh
# Start production environment
up-prod:
@echo "Starting production environment..."
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --build -d
Simulação do fluxo de eventos: publicação HTTP e consumo em tempo real
A simulação do sistema ocorre ao iniciar o ambiente com Docker Compose e, em seguida, publicar mensagens via HTTP no produtor. Quando o produtor envia o conteúdo ao Kafka, o broker persiste a mensagem no tópico configurado. O consumidor, inscrito nesse tópico, lê a mensagem e imprime o conteúdo em logs.
Como o Kafka pode levar alguns segundos para ficar pronto, os loops de reconexão nos microserviços reduzem falhas de inicialização. Além disso, o consumidor espera alguns segundos no startup antes de iniciar o consumo, o que evita tentativas prematuras. Esse comportamento é comum em ambientes containerizados com múltiplos serviços subindo simultaneamente.
Os comandos abaixo representam uma forma prática de simular o fluxo com as rotinas já definidas no Makefile e com uma chamada curl equivalente:
make up
make test-produce
make logs-b
curl -X POST http://localhost:8001/produce \
-H "Content-Type: application/json" \
-d '{"content":"Hello Kafka from FastAPI!"}'
Detalhes importantes de implementação: confiabilidade e comportamento do consumo
O uso de reconexão em loop trata o caso mais comum em Docker Compose: serviços dependerem do Kafka, mas o Kafka ainda não aceitar conexões. O depends_on apenas garante ordem de start, não o “ready state” do broker. O padrão de retry com asyncio.sleep mantém o serviço estável até a dependência ficar disponível.
No consumidor, o loop async for msg in consumer fornece mensagens na ordem da partição consumida. Em caso de exceção, o código volta a tentar após alguns segundos, evitando encerramento do processo. Para um comportamento mais robusto, costuma-se adicionar encerramento gracioso do consumer em shutdown, mas a base apresentada já demonstra a essência do streaming.
O auto_offset_reset="earliest" é útil em desenvolvimento para enxergar mensagens antigas quando um grupo ainda não possui offset salvo. Em produção, a escolha entre “earliest” e “latest” depende de requisitos, como reprocessamento ou apenas eventos novos. O group_id mantém o estado de consumo e permite paralelizar processamento com múltiplas instâncias.
Conclusão
O sistema apresentado consolida uma base clara de microserviços orientados a eventos com FastAPI, Kafka e Docker Compose. Um serviço produtor publica eventos via HTTP e um serviço consumidor processa esses eventos em tempo real, com comunicação desacoplada por tópico. A estrutura de pastas por microserviço, com Dockerfile próprio, reforça isolamento e facilita evolução independente.
O docker-compose centraliza a execução de infraestrutura e aplicações, padronizando rede, portas e dependências. Os códigos de produtor e consumidor demonstram integração assíncrona com Kafka usando aiokafka, incluindo tentativas de reconexão para lidar com o tempo de subida do broker. O resultado final é um exemplo completo e funcional de streaming de eventos, com início, meio e fim definidos na arquitetura e na execução.