Respondendo a um incidente de produção com a decisão de arquitetura que faltava, não só um patch
Identifiquei a causa raiz de um incidente de produção que bloqueava clientes como uma suposição de arquitetura não documentada, reconstruí o consumer afetado com um post-mortem, um ADR e documentação de regras de negócio, depois, após o rollout expor um deadlock de banco de dados, diagnostiquei-o como uma incompatibilidade de particionamento do Kafka e substituí a perda silenciosa de mensagens por retry, uma dead-letter queue e serialização por usuário.
impactoResolvi o incidente que bloqueava o cliente e documentei a decisão de arquitetura ausente para que a mesma lacuna não possa reabrir silenciosamente.Dei ao domínio um consumer devidamente consciente de workspace no serviço atual do time, onde nenhum existia antes.Eliminei o deadlock de produção em sua causa raiz (incompatibilidade de partition-key), não maquiando o sintoma.Substituí a perda silenciosa de mensagens por um caminho de falha recuperável, com retry para erros transitórios e uma dead-letter queue para os permanentes, fechando uma lacuna onde falhas antes desapareciam sem deixar rastro.Qualitativo: maior confiança na confiabilidade de um consumer crítico para permissões; nenhum incidente do mesmo tipo se repetiu após a correção.
01
Contexto
Esta é minha evidência mais clara de resposta a incidentes que corrige a classe do bug, não a instância, e de permanecer com um problema ao longo de dois atos: o incidente inicial, e o modo de falha mais sutil que só apareceu depois que a correção foi para produção. Nas duas vezes, resisti ao patch mais rápido disponível em favor de entender por que o sistema estava errado desde o início, e deixei a resposta documentada para que a próxima pessoa não precisasse redescobri-la.
Isso também mostra uma maturidade operacional que vai além de "corrigi o bug": diagnosticar um deadlock de produção a partir de logs, rastreando-o até uma incompatibilidade específica entre o particionamento de um sistema de mensageria e o padrão real de contenção dos dados, é raciocínio em nível de sistemas sob pressão, o tipo de debugging que separa "reiniciei o pod" de realmente entender a falha.
02
Problema
Um cliente ficou preso em uma tela permanente de "erro de sincronização" e não conseguia concluir seu trabalho, um bloqueio duro, não um bug cosmético. A causa próxima remontava a como um consumer de gestão de permissões tratava uma edição: uma decisão sobre se a lógica desse consumer deveria estar restrita a um workspace nunca tinha sido tornada explícita em lugar nenhum, então uma mudança que assumiu o escopo errado passou despercebida. O consumer subjacente para esse domínio também só existia no serviço legado do time; o serviço mais novo não tinha equivalente, o que era parte do motivo pelo qual a lacuna nunca tinha aparecido antes.
Meses depois que a reconstrução foi para produção, um segundo problema, aparentemente não relacionado, apareceu: o novo consumer começou a lançar deadlocks de banco de dados em produção.
03Restrições
+
—O defeito real não estava no caminho de código que falhou. Estava em uma decisão que nunca foi escrita. Corrigir o bug de escopo imediato teria deixado a mesma classe de erro possível na próxima mudança, porque nada registrava *por que* o consumer precisava se comportar da forma que deveria.
—As próprias palavras do usuário tornaram o impacto concreto. Ele estava bloqueado no meio de uma tarefa e frustrado; não havia ambiguidade sobre se isso importava.
—O deadlock era intermitente e não óbvio. Um erro genérico de "transação falhou" não dá nenhuma pista por si só de que a causa real é uma incompatibilidade entre como o trabalho é distribuído (particionamento) e como os dados subjacentes são de fato disputados.
—O tratamento de falhas existente piorava o deadlock. Mensagens com falha estavam sendo silenciosamente engolidas e seu offset confirmado mesmo assim, então antes mesmo de poder ser corrigido de verdade, o próprio modo de falha teve que mudar de "desaparece sem deixar rastro" para "visível e recuperável."
04
Decisão
Tratei o incidente como dois problemas de diagnóstico separados, meses um do outro, e recusei-me a encerrar qualquer um deles com uma correção superficial.
—Escrevi o post-mortem primeiro. Antes de reconstruir qualquer coisa, documentei o que aconteceu e por quê, para que o incidente tivesse um registro de responsabilidade, não só um ticket fechado.
—Tornei a decisão ausente explícita em um ADR. Ele cobre por que o consumer precisa respeitar o escopo de workspace, como deveria lidar com exclusões, como proteger a rastreabilidade de registros, e como casos extremos deveriam se comportar, em vez de codificar a correção só no código, onde a próxima pessoa teria que fazer engenharia reversa do raciocínio.
—Documentei as regras de negócio separadamente, porque elas estavam espalhadas pelo código de aplicação sem uma referência central, o que era parte do motivo pelo qual a lacuna original passou despercebida.
—Reconstruí o consumer propriamente no serviço e stack atuais do time, em vez de aplicar patch na implementação legada, já que o domínio ainda não tinha um equivalente lá.
—Meses depois, diagnostiquei o deadlock a partir de logs de produção, não por tentativa e erro. Busquei a assinatura de erro específica, encontrei rajadas repetidas, e confirmei o mecanismo: o tópico era particionado pelo identificador da própria mudança, não pelo usuário afetado, então múltiplas mensagens sobre o *mesmo* usuário podiam cair em partições diferentes e ser processadas simultaneamente, colidindo ao atualizar a mesma linha.
—Corrigi o mecanismo, não só o sintoma. Serializei atualizações por usuário afetado para que mensagens concorrentes sobre a mesma pessoa não pudessem mais competir entre si, e substituí o caminho de falha silenciosa por retry para erros transitórios e uma dead-letter queue para os permanentes, para que uma falha agora seja visível e recuperável em vez de invisível.
05Trade-offs
+
—Escrever um post-mortem, um ADR e documentação de regras de negócio em vez de entregar uma correção direta. Documentação tomou tempo real que o patch mais rápido não tomaria, mas a lacuna original existia *porque* a decisão nunca tinha sido escrita, e repetir esse erro teria custado mais depois do que economizou agora.
—Reconstruir o consumer na stack atual em vez de aplicar patch na legada. Um patch teria sido mais rápido, mas teria mantido o domínio dividido entre dois serviços sem uma única fonte da verdade, e deixado o serviço mais novo sem um comportamento de que precisava.
—Diagnosticar a causa raiz do deadlock em vez de adicionar um retry e chamar de resolvido. Um retry cego teria mascarado a colisão sem removê-la; rastrear a incompatibilidade até a partition key fez a correção endereçar a contenção real em vez de escondê-la.
—Serializar por usuário em vez de ampliar a transação ou o orçamento de retry. Estreitar a correção exatamente ao escopo da colisão (mesmo usuário, mensagens concorrentes) evitou uma lentidão mais ampla e vaga que uma correção mais defensiva teria introduzido em todo lugar.
06
Impacto
—Resolvi o incidente que bloqueava o cliente e documentei a decisão de arquitetura ausente para que a mesma lacuna não possa reabrir silenciosamente.
—Dei ao domínio um consumer devidamente consciente de workspace no serviço atual do time, onde nenhum existia antes.
—Eliminei o deadlock de produção em sua causa raiz (incompatibilidade de partition-key), não maquiando o sintoma.
—Substituí a perda silenciosa de mensagens por um caminho de falha recuperável, com retry para erros transitórios e uma dead-letter queue para os permanentes, fechando uma lacuna onde falhas antes desapareciam sem deixar rastro.
—Qualitativo: maior confiança na confiabilidade de um consumer crítico para permissões; nenhum incidente do mesmo tipo se repetiu após a correção.
07Lições Aprendidas
+
Conhecimento de engenharia reutilizável que levo adiante disso:
—Um incidente causado por uma decisão não documentada não está corrigido até que a decisão seja escrita. Do contrário, você corrigiu o sintoma e deixou a causa livre para ressurgir em outra forma.
—Um erro genérico de "transação falhou" é um ponto de partida, não um diagnóstico. A causa real costuma estar uma camada acima, aqui entre como o trabalho foi particionado e como os dados eram de fato disputados.
—Nunca deixe uma falha desaparecer silenciosamente. Um sistema que engole um erro e confirma mesmo assim é pior do que um que falha ruidosamente, porque você precisa conseguir ver uma falha antes de poder corrigir sua causa.
—Corrija o escopo exato da contenção, não toda a superfície ao redor. Serializar por usuário afetado resolveu a colisão real sem deixar tudo mais lento.
08Evidência
+
—Escrevi o post-mortem, o ADR e a documentação de regras de negócio do domínio, antes de reconstruir o consumer.
—Reconstruí o consumer no serviço e stack atuais do time; ativado em produção.
—Diagnostiquei de forma independente um deadlock de produção posterior como uma incompatibilidade de partition-key do Kafka via análise de logs, e substituí a perda silenciosa de mensagens por retry, dead-letter queue e serialização por usuário.
—Verificado contra o tracker. O incidente é datado de 2026-01-13, e a resposta foi decomposta na mesma semana: o post-mortem (2026-01-13 a 2026-01-19), o documento de regras de negócio do domínio (fechado 2026-01-29) e o ADR (2026-01-19 a 2026-01-28) foram todas tarefas rastreadas que *precederam* a reconstrução, não redações produzidas depois.
—Verificada a reconstrução: serviço base e integração de mensageria (2026-01-21 a 2026-01-26), os casos de uso de criação e exclusão (fechado 2026-02-17), um endpoint de soft-delete em lote para as relações afetadas, uma feature flag em staging (2026-02-17), ativação em produção (2026-03-16) com um erro de produção investigado e fechado no mesmo dia, e uma tarefa separada para reparar os usuários que o bug original já tinha corrompido (2026-01-16 a 2026-02-20).
—Verificado o acompanhamento, meses depois: uma exceção de validação de contrato de dados (2026-06-29), o deadlock de transação (2026-06-30 a 2026-07-01, quatro pull requests), uma inconsistência de ciclo de vida de usuário entre upsert e exclusão de papel (2026-07-07), e tratamento para o caso em que todos os papéis de um usuário são excluídos de uma vez.
—Fonte (privada): registro de incidente do Jira e seu post-mortem, ADR e tarefas de acompanhamento vinculados, tracker de engenharia da Dynamox; os pull requests correspondentes.