Voltar ao blog
12 de maio de 20268 min de leitura

Serviços Auxiliares e o Aprisionamento por Acoplamento

Uma squad constrói um worker de relatório. Faz sentido: ela precisa de relatórios. Com o tempo, o worker começa a conhecer o schema do banco da squad, os identificadores internos do domínio, as regras de formatação específicas daquele negócio. O worker e o serviço que o invoca ficam tão entrelaçados que qualquer manutenção em um exige conhecimento do outro. Agora só essa squad consegue mexer nos dois. E, quando outra squad precisa de relatórios, ela constrói o próprio worker do zero, com os mesmos bugs, sem o aprendizado acumulado.

Esse é o acoplamento forte disfarçado de autonomia. E ele não é um problema de arquitetura apenas: é um problema organizacional.

Guia de tópicos:

  • O Acoplamento que Aprisiona
  • Conway's Law: A Arquitetura que Imita a Comunicação
  • Coupling e Cohesion: O que a Literatura Diz
  • O Auxiliar que não Conhece os Dados
  • Quando o Compartilhamento se Torna Possível
  • Fork Estratégico: Herdar Maturidade em vez de Recriar Bugs
  • Eficiência de Infraestrutura como Consequência Natural
  • O Custo Organizacional do Comportamento Contrário
  • Exemplo Prático em Python
  • Considerações Finais

O Acoplamento que Aprisiona

Imagine dois serviços: um invocador que conhece os dados de negócio, e um worker auxiliar que processa esses dados. Se o worker precisa entender o significado dos dados para funcionar o schema, as regras de negócio, os identificadores de domínio então os dois serviços, na prática, são um só. A fronteira entre eles é ilusória.

O problema aparece quando outra squad tenta usar o mesmo worker. Ela não conhece os dados no formato que o worker espera. Não sabe quais campos são obrigatórios por razões implícitas. Não entende por que certos identificadores chegam em um formato específico. O worker não é um componente utilizável por qualquer equipe é uma extensão do sistema da squad original, com uma interface que parece genérica mas não é.

No pior cenário, a squad que criou o worker usa os dois softwares exclusivamente para manutenção mútua: mexe no invocador por causa de mudanças no worker, mexe no worker por causa de mudanças no invocador. O acoplamento virou um ciclo fechado. Compartilhamento se torna impossível não por falta de vontade, mas por impossibilidade técnica qualquer outra squad teria que entrar nesse ciclo também.


Conway's Law: A Arquitetura que Imita a Comunicação

Em 1968, Melvin Conway observou que organizações inevitavelmente produzem sistemas cuja estrutura espelha sua estrutura de comunicação. Se uma squad controla o invocador e o auxiliar, o sistema resultante vai refletir a comunicação interna dessa squad que é densa, informal e cheia de contexto implícito. O worker vai conhecer o banco de dados da squad porque o desenvolvedor que criou o worker também conhece o banco de dados da squad e achou mais fácil acessar diretamente.

O inverso de Conway também funciona como ferramenta de design. Se você quer que um serviço seja utilizável por múltiplas squads, ele precisa ter uma interface que pode ser construída e mantida sem comunicação informal com a squad de origem. Isso exige separação clara de responsabilidades antes mesmo de escrever código uma decisão organizacional que se manifesta como decisão técnica.

Team Topologies (Skelton e Pais, 2019) formaliza isso: serviços auxiliares compartilhados pertencem ao modelo de plataforma, em que o objetivo explícito é reduzir a carga cognitiva das squads consumidoras. Uma plataforma falha quando exige que as squads consumidoras entendam seus internos para usá-la.


Coupling e Cohesion: O que a Literatura Diz

Yourdon e Constantine, em Structured Design (1979), definiram o par que ainda orienta design de software décadas depois: alta coesão, baixo acoplamento.

Coesão mede o quanto as responsabilidades dentro de um módulo pertencem juntas. Acoplamento mede o quanto um módulo depende do estado interno ou da implementação de outro. O ideal é que cada módulo faça bem uma coisa (alta coesão) sem precisar que outros módulos revelem seus detalhes internos (baixo acoplamento).

Um worker auxiliar com alta coesão sabe fazer exatamente uma coisa gerar relatórios, enviar notificações, processar filas e faz bem. Um worker com baixo acoplamento recebe os dados que precisa pela interface, sem buscar contexto adicional em outros serviços. A combinação dos dois é o que transforma um serviço interno em um componente genuinamente reutilizável.

O acoplamento forte que aprisiona uma squad é, na terminologia de Yourdon e Constantine, acoplamento de conteúdo o tipo mais severo, onde um módulo depende do funcionamento interno de outro. Não do contrato, não da interface: do interior.


O Auxiliar que não Conhece os Dados

A separação correta é esta: o auxiliar sabe como processar. O invocador sabe o quê processar.

Um worker de relatório que recebe {title, columns, rows, format} e devolve um arquivo não precisa saber que aquelas linhas representam vendas do trimestre. Não precisa saber que a coluna val é o valor em reais antes de impostos. Não precisa saber que o campo uid é um identificador de usuário que pode ser resolvido em outro serviço.

Quem sabe disso é o invocador. Ele busca os dados, monta a estrutura, chama o worker com tudo que o worker precisa para executar. O conhecimento de negócio fica onde pertence: no domínio que o possui.

Essa separação tem um efeito direto na maturidade. Um worker sem regras de negócio estabiliza rápido. Depois de um período inicial de ajustes na interface e na implementação, ele para de mudar porque o negócio continua mudando no invocador, não nele. A manutenção cai para próximo de zero. O worker vira infraestrutura.


Quando o Compartilhamento se Torna Possível

Um worker que não conhece os dados pode ser invocado por qualquer squad que consiga montar a estrutura de entrada. Não é necessário entrar em contato com a squad de origem para entender comportamentos implícitos. Não é necessário ler o código interno para descobrir por que certos campos são obrigatórios. O contrato é suficiente.

Isso cria um subproduto organizacional valioso: comunicação contínua entre squads que compartilham o mesmo componente. Mudanças no contrato precisam ser coordenadas. Melhorias beneficiam todos simultaneamente. Bugs encontrados por uma squad são corrigidos para todas. O worker compartilhado vira um ponto de alinhamento técnico entre times que normalmente não se comunicam não por design explícito, mas porque a dependência compartilhada força a conversa.

Domain-Driven Design chama isso de shared kernel: um componente que pertence a múltiplos bounded contexts e exige coordenação explícita para evoluir. A diferença entre um shared kernel funcional e um que gera conflito é a clareza do contrato. Contratos implícitos explodem quando mais de uma squad tenta manter o mesmo componente.


Fork Estratégico: Herdar Maturidade em vez de Recriar Bugs

Há casos em que o compartilhamento não é viável. Infraestruturas diferentes, requisitos de compliance, restrições de rede entre ambientes. Nesses casos, a resposta correta não é construir do zero, é fazer um fork.

Um fork de um worker maduro transfere o aprendizado acumulado: edge cases tratados, comportamentos defensivos, otimizações que só aparecem em produção sob carga real, documentação de limitações descobertas da forma difícil. A squad que copia herda esse histórico sem ter passado pelos incidentes que o geraram.

Construir do zero é, na prática, pagar o custo de todos esses aprendizados novamente com o agravante de que os bugs vão aparecer em produção, não em um repositório de histórico de commits de outra squad. O fork cria uma linha de desenvolvimento independente que pode divergir conforme as necessidades divergem. Isso é aceitável. O que não é aceitável é recriar os mesmos problemas por orgulho de não depender de código externo.


Eficiência de Infraestrutura como Consequência Natural

Quando múltiplas squads implementam o mesmo worker de forma independente, cada implementação precisa de pelo menos um pod em standby durante períodos de baixo uso. Em uma empresa com dez squads cada uma com seu próprio worker de relatório, são dez pods ociosos à noite onde poderia haver um.

Esse custo parece marginal por pod. Multiplicado por todos os serviços auxiliares duplicados de uma empresa média, o custo de compute se torna relevante. O custo de gestão monitoramento, alertas, on-call, rotação de certificados, atualização de dependências com vulnerabilidades é ainda mais relevante porque é custo humano, não de máquina.

Em pico, a lógica se inverte favoravelmente: o worker compartilhado escala para absorver carga agregada de todas as squads. Cada implementação isolada escalaria sozinha, sem compartilhar capacidade com as demais.


O Custo Organizacional do Comportamento Contrário

Quando squads aprisionam seus serviços auxiliares por acoplamento, três problemas organizacionais se instalam silenciosamente.

Conhecimento se descompartimentaliza. O worker que só a squad A consegue manter carrega conhecimento implícito distribuído entre dois ou três desenvolvedores. Quando eles saem, o componente vira uma caixa preta com interface mal documentada. Ninguém toca por medo. Ele envelhece sem evolução.

Análise de domínio fica contaminada por premissas locais. O desenvolvedor que construiu e mantém um worker acoplado ao próprio contexto tende a não perceber que uma abstração mais genérica seria possível e superior. O acoplamento limita a visão do que o componente poderia ser se fosse desenhado para ser compartilhado desde o início.

Possibilidades de expansão ficam invisíveis. Um worker maduro e compartilhado é uma plataforma; ele cresce com contribuições de múltiplas squads, cada uma adicionando casos de uso que as demais aproveitam. Um worker aprisionado cresce apenas quando a squad proprietária tem tempo e interesse. O ritmo de evolução de uma plataforma é estruturalmente diferente do ritmo de evolução de um componente isolado.


Exemplo Prático em Python

from dataclasses import dataclass
from typing import Any

# Contrato explícito: o worker recebe tudo que precisa, não busca nada
@dataclass
class ReportRequest:
    title: str
    columns: list[str]
    rows: list[list[Any]]
    output_format: str = "pdf"

class ReportWorker:
    """Alta coesão: só sabe gerar relatórios.
    Baixo acoplamento: não conhece nenhum domínio de negócio."""

    def generate(self, request: ReportRequest) -> bytes:
        match request.output_format:
            case "csv":  return self._as_csv(request)
            case "pdf":  return self._as_pdf(request)
            case _: raise ValueError(f"Unsupported format: {request.output_format}")

    def _as_csv(self, r: ReportRequest) -> bytes:
        lines = [",".join(r.columns)]
        lines += [",".join(str(v) for v in row) for row in r.rows]
        return "\n".join(lines).encode()

    def _as_pdf(self, r: ReportRequest) -> bytes:
        lines = [r.title, "=" * len(r.title), " | ".join(r.columns)]
        lines += [" | ".join(str(v) for v in row) for row in r.rows]
        return "\n".join(lines).encode()


# Squad A: conhece dados de vendas, monta o contrato
def squad_a_invoke(worker: ReportWorker) -> bytes:
    sales_data = [["Q1", 120_000], ["Q2", 145_000]]  # vem do domínio da squad A
    return worker.generate(ReportRequest(
        title="Relatório de Vendas",
        columns=["Trimestre", "Receita (R$)"],
        rows=sales_data,
    ))

# Squad B: conhece dados de logística, usa o mesmo worker sem saber nada de vendas
def squad_b_invoke(worker: ReportWorker) -> bytes:
    shipment_data = [["SP", 342], ["RJ", 198]]  # vem do domínio da squad B
    return worker.generate(ReportRequest(
        title="Relatório de Envios",
        columns=["UF", "Envios"],
        rows=shipment_data,
        output_format="csv",
    ))

worker = ReportWorker()
print(squad_a_invoke(worker).decode())
print("---")
print(squad_b_invoke(worker).decode())

O worker não muda quando o modelo de negócio da squad A ou da squad B muda. Apenas o invocador muda, que é exatamente quem conhece o negócio.


Considerações Finais

O acoplamento forte entre um serviço auxiliar e seu invocador não é um problema técnico isolado: é um problema organizacional que se manifesta como problema técnico. Ele aprisiona conhecimento em uma única squad, impede o compartilhamento, multiplica custos de infraestrutura e inibe a percepção de possibilidades de expansão.

A solução não é nova. Yourdon e Constantine as descreveram em 1979. Conway previu em 1968. Skelton e Pais a formalizaram para o contexto de times modernos em 2019. A literatura converge no mesmo princípio: módulos que sabem fazer uma coisa bem, sem depender dos internos de outros módulos, são os que sobrevivem, escalam e se tornam plataforma.

O worker que não conhece os dados é, paradoxalmente, o worker que serve mais squads. A ignorância deliberada é o que torna o compartilhamento possível.

Observações:

Há ressalvas práticas sobre o transporte dos dados. Enviar milhares de registros via payload HTTP tem custo real, latência, limite de tamanho, pressão de memória nos dois lados. Nesses casos, o contrato não muda de natureza, muda de canal: uma fila de mensagens, um stream via Kafka, um arquivo em object storage com referência passada na requisição. O invocador ainda é o único que conhece os dados e decide como entregá-los. O worker ainda processa sem precisar buscá-los em outro lugar. A lógica de separação se mantém; só o meio de transporte se adapta à escala.

Links:

  • Conway's Law: https://www.melconway.com/Home/Conways_Law.html
  • Structured Design (Yourdon & Constantine): https://en.wikipedia.org/wiki/Structured_Design
  • Team Topologies: https://teamtopologies.com
  • Domain-Driven Design — Shared Kernel: https://martinfowler.com/bliki/BoundedContext.html