Programação

Construindo uma plataforma de e-commerce com microserviços

Como pensei, estruturei e implementei uma plataforma de e-commerce com serviços independentes em GoLang e Python, orquestrados via Docker Compose.

Esse projeto nasceu de uma pergunta simples: como seria construir um sistema real onde cada parte tem vida própria?

Eu já tinha trabalhado em monolitos e entendia a comodidade deles, tudo num lugar só, fácil de rodar, fácil de debugar. Mas microserviços sempre apareciam em vagas, arquiteturas de grandes empresas e conversas técnicas. Decidi parar de só ler a respeito e construir algo do zero.


O que eu quis construir

Uma plataforma de e-commerce funcional, com quatro responsabilidades bem definidas:

  • Catálogo de produtos, listagem e consulta de itens disponíveis
  • Autenticação de usuários, registro, login com JWT e refresh token
  • Carrinho de compras, estado temporário por usuário
  • Pedidos, criação e consulta do histórico de compras

Cada uma dessas responsabilidades virou um serviço independente. Banco de dados próprio, container próprio, porta própria.


As decisões de tecnologia

GoLang no serviço de produtos

Escolhi Go para o product-service por um motivo específico: queria aprender a linguagem em contexto real, não apenas com exercícios isolados. Produtos é o serviço com maior volume de leitura numa plataforma de e-commerce, faz sentido usar uma linguagem compilada e eficiente ali.

O servidor sobe em milissegundos, os tipos são explícitos e o compilador é implacável. Aprendi rápido que Go não te deixa ignorar erros, cada operação que pode falhar precisa ser tratada explicitamente:

err := db.QueryRow(
    "SELECT id, name, description, price, stock FROM products WHERE id = $1", id,
).Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock)

if err == sql.ErrNoRows {
    c.JSON(http.StatusNotFound, gin.H{"message": "Produto não encontrado"})
    return
}

Vindo de Python, onde exceções são silenciosas até explodir, isso foi uma mudança de mentalidade bem vinda.

Python + FastAPI para os outros três serviços

Para autenticação, pedidos e carrinho usei Python com FastAPI. A escolha foi pragmática, já tinha familiaridade e queria avançar nas partes mais complexas de cada domínio sem travar na sintaxe.

FastAPI tem uma vantagem clara: a documentação interativa gerada automaticamente (Swagger UI) tornava o teste de cada endpoint muito mais rápido durante o desenvolvimento.

Redis só para o carrinho

O carrinho foi o serviço que mais exigiu que eu pensasse sobre o tipo dos dados, não só o modelo deles.

Itens de carrinho são dados temporários. O usuário adiciona, remove, abandona o site, volta amanhã. Não precisam de um banco relacional com transações e foreign keys, precisam de velocidade e simplicidade. Redis resolveu isso com uma linha por operação:

# salvar
r.set(f"cart:{user_id}", json.dumps([i.dict() for i in cart.items]))

# recuperar
cart_data = r.get(f"cart:{user_id}")

Cada carrinho é uma chave no Redis. Sem migrations, sem joins, sem overhead.


O problema que ninguém menciona nos tutoriais

O maior desafio prático não foi a lógica de negócio, foi a ordem de inicialização dos containers.

Quando você sobe tudo com docker-compose up, o PostgreSQL leva alguns segundos para ficar pronto. Se um serviço tentar conectar antes disso, o processo morre. Sem retry, a aplicação toda falha na subida.

A solução foi implementar um loop de tentativas em cada serviço. Em Go ficou assim:

for i := 1; i <= 10; i++ {
    db, err = sql.Open("postgres", dbURL)
    if err == nil {
        if pingErr := db.Ping(); pingErr == nil {
            log.Println("Database connection successful")
            return
        }
    }
    log.Printf("DB not ready (attempt %d/10)\n", i)
    time.Sleep(3 * time.Second)
}
log.Fatal("Could not connect to database after multiple retries")

No Docker Compose, complementei com healthcheck no serviço do banco e depends_on com condition: service_healthy nos serviços que dependem dele. Os dois mecanismos juntos garantem que nenhum serviço sobe antes de sua dependência estar de fato pronta, não apenas iniciada.


Autenticação com access token e refresh token

O user-auth-service foi o mais estruturado do projeto. Implementei autenticação com JWT usando dois tokens:

  • Access token, curta duração, usado em cada requisição
  • Refresh token, longa duração, usado só para gerar um novo access token
@router.post("/refresh", response_model=Token)
async def refresh(body: RefreshRequest, db: AsyncSession = Depends(get_db)):
    payload = decode_token(body.refresh_token)
    if not payload or payload.get("type") != "refresh":
        raise HTTPException(status_code=401, detail="Invalid refresh token")
    # ...gera novo par de tokens

O campo "type" no payload do JWT garante que um access token não pode ser usado como refresh e vice-versa. É um detalhe pequeno que evita uma classe de vulnerabilidades.

Senhas são armazenadas com bcrypt via passlib. Nunca toquei no hash diretamente, a biblioteca abstrai a comparação segura.


Banco de dados separado por serviço

Cada serviço que usa PostgreSQL tem seu próprio banco, criado por um script de inicialização montado no container:

postgres:
  volumes:
    - ./infra/db/create_databases.sh:/docker-entrypoint-initdb.d/01_create_databases.sh

Isso reflete um princípio importante de microserviços: nenhum serviço acessa o banco do outro. Se o order-service precisa de dados de um produto, ele chama a API do product-service, não consulta a tabela diretamente. Essa separação garante que cada serviço pode evoluir o seu schema de forma independente.


O que aprendi

Microserviços não são sobre tecnologia, são sobre fronteiras. A parte difícil não é subir containers, é decidir onde um serviço termina e outro começa. Errar essa fronteira cria serviços que se acoplam de volta e você acaba com um monolito distribuído, que tem todos os problemas de um monolito mais a latência de rede.

Docker Compose é suficiente para desenvolvimento, mas exige atenção. O depends_on básico não espera o serviço estar pronto, apenas iniciado. Sem healthcheck, você está torcendo para a ordem dos logs dar certo. Com healthcheck, você tem garantia.

Usar duas linguagens no mesmo projeto é mais valioso do que parece. Não porque Go seja "melhor" que Python ou vice-versa, mas porque te força a separar o que é característica da linguagem do que é lógica do problema. Você para de confundir sintaxe com conceito.

Redis muda como você pensa sobre estado. Antes desse projeto, eu usaria uma tabela SQL para tudo. Após implementar o carrinho com Redis, ficou claro que a escolha do banco precisa ser orientada pela natureza dos dados, não por hábito.


O projeto está disponível no https://github.com/jcvm-dev/ecommerce_microsservices com instruções para rodar tudo localmente com um único docker-compose up --build.