# Rafael Pereira — Senior Software Engineer - Complete Documentation This file contains all documentation concatenated into a single file for easy consumption by LLMs. > Institutional portfolio and editorial hub of Rafael Pereira, senior software engineer: proof-first case studies, project artifacts, technical articles, experience narratives, resume, and contact. ## Table of Contents This document includes all content from this project. Each section is separated by a horizontal rule (---) for easy parsing. --- # A maioria dos problemas de performance de backend começa perto dos dados URL: https://imrafaeldev.site/artigos/backend-performance-perto-dos-dados > Antes de subir máquina, cache ou fila, meça o trabalho da requisição. Plano de execução, N+1, CAST em coluna e loops sequenciais. - [Início](/) - [Artigos](/artigos/) - A maioria dos problemas de performance de backend começa perto dos dados ## A maioria dos problemas de performance de backend começa perto dos dados API lenta e a reunião já enche de soluções. Eu começo perto dos dados — não por culpar o banco, mas porque essa verificação costuma dar sinal rápido. 16 de julho de 2026 - [Performance](/artigos/?topic=performance) - [Arquitetura](/artigos/?topic=arquitetura) Uma API fica lenta e a reunião já enche de soluções antes de ganhar uma medição. Aparecem mais CPU e RAM, cache, fila, microserviços e refatoração. Às vezes alguém propõe até trocar a linguagem. Raramente a primeira sugestão é abrir o plano de execução. Por isso, começo a investigação perto dos dados. Não porque o banco seja o culpado por padrão, mas porque muita coisa passa por ali e essa verificação costuma dar sinal rápido. Se a consulta estiver saudável, tiro o banco da frente e sigo o fluxo da requisição. ## Soluções rápidas também escondem trabalho caro Cache e fila resolvem problemas reais. Mais máquina também pode ser a decisão certa. Quando entram por reflexo, porém, esses recursos podem apenas mudar o tamanho da conta enquanto ninguém sabe qual trecho da requisição está segurando a resposta. Subir CPU reduz a disputa por recurso, cache tira algumas requisições do caminho e fila absorve um pico. Enquanto isso, uma consulta que faz um trabalho enorme para retornar quase nada continua cara em cada execução. O mesmo vale para um loop que dispara uma chamada por item. A latência pode cair por um tempo, o alerta para de tocar e o time respira. Quando a carga cresce, a operação cara reaparece, agora acompanhada de uma infraestrutura maior. A pergunta que precisa vir antes da discussão de arquitetura: quanto trabalho essa requisição está produzindo para entregar o resultado? ## Quase dobramos a máquina e o dashboard continuou lento Foi o que aconteceu em um dashboard em que trabalhei. Ele carregava de forma síncrona, e uma única consulta fazia tudo de uma vez: buscava os dados, cruzava várias informações e calculava os valores exibidos. Como a CPU do banco ficava muito alta, quase dobramos CPU e RAM. O banco ficou maior. O dashboard continuou passando de um minuto para carregar. A mesma consulta ainda concentrava todo o trabalho pesado em uma única execução. Foi quando abrimos o plano de execução. A tela devolvia poucos indicadores, mas a consulta atravessava relacionamentos, formava um volume intermediário grande e gastava CPU em agregações antes de chegar neles. A resposta final era pequena. O trabalho para produzi-la, enorme. Dobrar CPU e RAM tinha dado mais fôlego ao banco, mas a busca continuava obrigando-o a fazer tudo de uma vez. O plano mostrou que a investigação precisava entrar no caminho percorrido pela consulta. ## O que preciso ver antes de mexer no banco Para o banco virar suspeito de verdade, quero o plano de execução e as métricas apontando nessa direção. Tempo da consulta, leituras e cardinalidade geralmente confirmam ou eliminam uma hipótese em poucos minutos. Se essas medidas estiverem saudáveis, tiro o banco da frente. Já peguei API lenta com a consulta respondendo dentro do esperado, enquanto o atraso estava no processamento da aplicação e em chamadas remotas feitas de forma sequencial. A partir daí, continuar procurando um defeito no banco seria apenas insistir na camada errada. Antes de aprofundar, passo um radar curto pela consulta e pelo código. Uma query para carregar a lista seguida de outra para cada registro pede uma contagem de consultas. Esse N+1 aparece com frequência quando o ORM deixa as relações para o backend buscar uma a uma. Um JOIN que multiplica linhas antes da agregação pede a medição do volume intermediário. Índice ausente pede plano. Um filtro que aplica uma função sobre a coluna indexada também. No código, procuro chamadas remotas ou consultas com await dentro de for. Cada espera pode parecer pequena isoladamente e ainda assim dominar o tempo total quando todas executam em fila. Nenhum desses sinais fecha o diagnóstico. Um N+1 numa rota com dois itens pode ter impacto irrelevante, um índice novo pode ajudar pouco numa coluna com baixa seletividade e um join volumoso talvez seja necessário para produzir o resultado. Por isso, conto as consultas, cronometro o loop inteiro e confiro no plano quantas linhas e leituras foram produzidas. Esse radar apenas escolhe a primeira medição. A próxima camada da investigação vem da conta. ## Uma linha correta pode desperdiçar o índice Uma dessas linhas costuma passar batida na revisão. Ela devolve os registros de um dia inteiro. WHERE CAST(campo_data_hora AS date) = @data O resultado da tela parece correto. No plano, a história pode ser outra. Aplicar CAST à coluna obriga a consulta a transformar os valores antes da comparação. Com um índice em campo_data_hora, isso pode impedir uma busca direta pelo intervalo, aumentar bastante as leituras e até levar a um scan. Quando o pedido é pelos registros de um dia inteiro, calculo as bordas fora da coluna. WHERE campo_data_hora >= @inicio_do_dia AND campo_data_hora < @inicio_do_proximo_dia Se o início é 16 de julho à meia-noite, o limite seguinte é 17 de julho à meia-noite. Assim entram todos os valores do dia 16, inclusive aqueles com frações de segundo no final, sem depender de 23:59:59.999. O resultado permanece correto, mas agora existe um intervalo que o índice pode percorrer. A confirmação vem da comparação das leituras e do operador de acesso nos dois planos. Se a métrica não mudar, a hipótese não ficou de pé. ## Leia o plano pela sequência do trabalho Depois de comparar as versões do filtro, leio o plano como uma história do trabalho produzido pela consulta. Começo pelo tempo total e pelas leituras. Se a tela devolve dez indicadores, mas a execução faz centenas de milhares de leituras, existe uma conta para explicar. Depois comparo a cardinalidade estimada com a real. Quando o otimizador esperava poucas linhas e recebeu muitas, ele pode ter escolhido joins, memória e agregações para um cenário bem menor do que encontrou durante a execução. Em seguida acompanho onde os dados crescem. Procuro o operador em que um JOIN leva milhares de linhas a centenas de milhares, pouco antes de um filtro ou GROUP BY reduzir tudo novamente. A resposta final pequena pode esconder um volume intermediário enorme. É nesse caminho que agregações e ordenações começam a gastar CPU demais. Também não tomo o percentual de custo exibido pelo plano como sentença. Ele serve para escolher onde medir. Marco o operador caro, quantas linhas entram e saem dele e quanto tempo ele consome. Se o custo aparece depois da multiplicação das linhas e antes dos poucos indicadores necessários, já existe uma hipótese concreta para testar. ## Quebrar a consulta onde o custo cresce O que resolveu o dashboard foi quebrar a consulta e aproveitar os índices corretamente. A divisão não aconteceu de forma arbitrária. O próprio plano mostrou onde o custo começava a crescer. Mantivemos no banco os filtros e a busca indexada dos registros necessários. Levar esse recorte para a aplicação faria o servidor receber um conjunto maior antes de poder descartá-lo. Também desperdiçaria o caminho que os índices já encurtavam. A composição final dos indicadores foi para a aplicação. Essa etapa vinha depois dos relacionamentos e das agregações que elevavam o volume intermediário e mantinham alta a CPU do banco. Com os dados já recortados, o servidor podia montar os valores da tela sem concentrar toda a execução em uma única query. O banco passou a reduzir o conjunto cedo. A aplicação recebia os registros selecionados e compunha os indicadores. Depois da mudança, o dashboard deixou de depender daquela consulta pesada e ficou mais previsível, porque a divisão acompanhava o ponto em que o custo crescia no plano. Essa fronteira não veio de uma preferência por query única ou por mais lógica na aplicação. --- # Desenferrujando a lógica #02: Container With Most Water URL: https://imrafaeldev.site/artigos/desenferrujando-logica-container-with-most-water > LeetCode 11 em JavaScript: uma tentativa baseada nos vizinhos, a revisão da hipótese e a solução de dois ponteiros. - [Início](/) - [Artigos](/artigos/) - Desenferrujando a lógica #02: Container With Most Water ## Desenferrujando a lógica #02: Container With Most Water Eu tentei escolher o próximo passo olhando apenas para os vizinhos. Funcionou em alguns casos, mas o problema pedia uma visão mais ampla. 15 de setembro de 2026 - [Performance](/artigos/?topic=performance) - [Trade-offs](/artigos/?topic=trade-offs) Depois de resolver o primeiro exercício da série, continuei no LeetCode para trabalhar a lógica sem pedir uma solução pronta. O desafio 011 é o [Container With Most Water](https://leetcode.com/problems/container-with-most-water/). Recebemos um array de alturas e precisamos escolher duas linhas que formem o recipiente com a maior área. ## Como calcular a área Se escolho as posições left e right, a largura é a distância entre elas. A altura do recipiente é limitada pela menor das duas linhas. área = min(altura da esquerda, altura da direita) × distância Por exemplo, com estas alturas: [1, 8, 6, 2, 5, 4, 8, 3, 7] As linhas nas posições 1 e 8 têm alturas 8 e 7. A menor altura é 7, e a distância entre elas é 7. Essa combinação produz uma área de 49. ## Minha primeira tentativa Comecei com dois ponteiros, um em cada ponta do array. Depois de calcular a área atual, eu simulava duas possibilidades: - avançar o ponteiro da esquerda; - recuar o ponteiro da direita. Eu calculava a área dos dois próximos pares e escolhia o maior. O trecho principal era este: const paddingLeftArea = Math.min(heights[leftIndex + 1], heights[rigthIndex]) * (rigthIndex - leftIndex + 1); const paddingRightArea = Math.min(heights[leftIndex], heights[rigthIndex - 1]) * (rigthIndex - 1 - leftIndex); if (paddingLeftArea > paddingRightArea && paddingLeftArea > maxArea) { leftIndex += 1; } else { rigthIndex -= 1; } O problema estava na hipótese. A melhor decisão local não garante a melhor área no restante do array. Eu tentava adivinhar o caminho olhando apenas para os dois próximos movimentos. Também havia um erro na fórmula da distância desse rascunho: para um par de posições, a largura é right - left. ## A observação que destrava o problema A área depende de duas coisas: largura e menor altura. Quando os ponteiros estão nas posições left e right, mover o ponteiro da maior altura não pode aumentar a altura mínima do recipiente. A largura sempre diminui, e a altura que limita a área continua presente. Por isso, o ponteiro que deve avançar é o da menor altura. É o único movimento que pode encontrar uma linha mais alta e compensar a perda de largura. Se as alturas forem iguais, qualquer um dos dois pode avançar. No código, escolhi avançar o da esquerda quando heights[leftIndex] <= heights[rigthIndex]. ## Solução com dois ponteiros /** * @param {number[]} heights * @return {number} */ var maxArea = function (heights) { let leftIndex = 0; let rigthIndex = heights.length - 1; let maxArea = 0; while (leftIndex < rigthIndex) { const minH = Math.min(heights[leftIndex], heights[rigthIndex]); const currentArea = minH * (rigthIndex - leftIndex); maxArea = Math.max(maxArea, currentArea); if (heights[leftIndex] <= heights[rigthIndex]) { leftIndex++; } else { rigthIndex--; } } return maxArea; }; A cada rodada, calculo a área do par atual, atualizo a maior área encontrada e movo um dos ponteiros. O while se aproxima do centro e termina. O detalhe do avanço importa. Na versão que eu havia escrito, os ponteiros só avançavam quando a área atual não era maior que maxArea. Se uma nova área máxima fosse encontrada, a mesma combinação seria calculada de novo, sem sair do loop. A correção foi separar as duas decisões: registrar a área e, depois, mover o ponteiro da menor altura. ## O resultado das tentativas O histórico do LeetCode ficou assim: - JavaScript: aceita, 3 ms e 63.6 MB. - JavaScript: resposta incorreta. - JavaScript: resposta incorreta. - Go: aceita, 0 ms e 9.6 MB. - TypeScript: aceita, 3 ms e 63.9 MB. - Go: resposta incorreta. Foram três tentativas com resposta incorreta antes de chegar às soluções aceitas em JavaScript, Go e TypeScript. Mais do que contar submissões, eu queria olhar para o erro, entender a hipótese que falhou e tentar de novo sem terceirizar todo o raciocínio. ## Complexidade O algoritmo percorre o array uma vez. Em cada iteração, um dos ponteiros avança, então a complexidade de tempo é O(n) e a complexidade de espaço é O(1). A primeira tentativa também usava dois ponteiros, mas fazia trabalho extra para comparar possibilidades futuras. A segunda solução usa uma propriedade do problema para descartar com segurança parte das combinações. Esse foi o exercício desta vez: não confundir uma escolha que parece boa agora com uma decisão que o problema realmente permite justificar. --- *Série Desenferrujando a lógica #02 — Container With Most Water. Problema em [leetcode.com/problems/container-with-most-water](https://leetcode.com/problems/container-with-most-water/).* Geometria: min da altura × largura Nas posições 1 e 8, a menor altura é 7 e a largura é 7. O recipiente produz área 49. Dois ponteiros: mova a menor linha A largura sempre diminui. Só mover a menor altura pode encontrar um limite maior; mover a maior mantém o gargalo e pode ser descartado. --- # Desenferrujando a lógica #01: Group Anagrams URL: https://imrafaeldev.site/artigos/desenferrujando-logica-group-anagrams > LeetCode 49 em Go: da chave por sort à contagem de 26 letras, e o hábito de continuar pensando depois que o código funciona. - [Início](/) - [Artigos](/artigos/) - Desenferrujando a lógica #01: Group Anagrams ## Desenferrujando a lógica #01: Group Anagrams Eu não tinha parado de escrever código. O que mudou foi terceirizar partes do raciocínio. Group Anagrams foi o exercício para recuperar o hábito. 17 de agosto de 2026 - [Trade-offs](/artigos/?topic=trade-offs) - [Performance](/artigos/?topic=performance) Depois de anos programando, percebi que minha lógica estava enferrujada. Eu não tinha parado de escrever código. O que mudou foi que, aos poucos, comecei a terceirizar partes do raciocínio que antes precisava exercitar sozinho. Escolhi o exercício [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/) no LeetCode. A proposta é receber uma lista de strings e reunir os anagramas no mesmo grupo. ## O que precisamos resolver Entrada: ["eat", "tea", "tan", "ate", "nat", "bat"] Resultado possível: ["eat", "tea", "ate"] ["tan", "nat"] ["bat"] A ordem dos grupos não importa. ## O que é um anagrama? Pegue eat, tea e ate. Cada uma tem a uma vez, e uma vez e t uma vez. A posição muda; a quantidade de cada letra continua igual. O algoritmo precisa transformar essas palavras em uma representação comum. Se as três produzirem a mesma chave, posso usar essa chave em um map e colocá-las no mesmo grupo. O primeiro problema é criar essa chave. ## Minha primeira resposta foi ordenar Comecei usando a string ordenada como chave: eat → aet tea → aet ate → aet As três produzem aet. ### Solução usando sort Essa foi a primeira solução que me ocorreu. Eu não estava tentando a implementação mais enxuta de imediato. Queria montar uma solução coerente e entender onde ela poderia melhorar. func sortString(str string) string { b := []byte(str) slices.Sort(b) return string(b) } func groupAnagrams(strs []string) [][]string { mapping := make(map[string][]string) for _, str := range strs { sortedStr := sortString(str) mapping[sortedStr] = append(mapping[sortedStr], str) } result := make([][]string, 0, len(mapping)) for _, group := range mapping { result = append(result, group) } return result } O que acontece nesse código: - sortString(str) transforma a string em bytes, ordena os caracteres e devolve uma nova string. - sortedStr := sortString(str) produz a chave daquele termo. - mapping[sortedStr] = append(...) usa essa chave para acumular os anagramas no mesmo grupo. - Para eat, tea e ate, sortedStr será sempre aet. O mapa começa a ficar assim: "aet" -> ["eat", "tea", "ate"] "ant" -> ["tan", "nat"] "abt" -> ["bat"] Calculo uma chave e adiciono a palavra diretamente ao grupo correspondente. O map evita comparar cada string com todas as outras. ## Onde está o custo dessa abordagem? Para descobrir a chave, preciso ordenar cada string. Se uma string tem k caracteres, essa ordenação custa aproximadamente O(k log k). Repetindo para n strings, a parte dominante fica em O(n × k log k). ### Por que retirar o sort? Para eat, eu ordenava os caracteres para chegar em aet. Para tea, fazia outra ordenação para chegar no mesmo aet. O sort funciona porque cria uma representação comum. Só que o problema não exige ordenar nada. Para saber se duas strings são anagramas, basta verificar se possuem a mesma quantidade de cada letra. ## Essa observação muda a solução Em vez de colocar as letras na mesma ordem, posso contar quantas vezes cada uma aparece. Nesse exercício, as entradas usam letras minúsculas de a até z. Cada string pode ser representada por 26 contadores. tea e ate produzem a mesma contagem. Não preciso reorganizar nenhum caractere. Só percorro a string e conto as ocorrências. Para eat, a parte relevante fica: a = 1 e = 1 t = 1 ### Solução usando contagem var key [26]uint8 cria 26 posições, uma para cada letra de a até z. key[str[i]-'a']++ encontra a posição de cada caractere e incrementa o contador. Depois, groups[key] = append(groups[key], str) usa o próprio vetor de frequências como chave do grupo. func groupAnagrams(strs []string) [][]string { groups := make(map[[26]uint8][]string, len(strs)) for _, str := range strs { var key [26]uint8 for i := 0; i < len(str); i++ { key[str[i]-'a']++ } groups[key] = append(groups[key], str) } result := make([][]string, 0, len(groups)) for _, group := range groups { result = append(result, group) } return result } ## O que mudou em complexidade? - Ordenação: O(k log k) por string e O(n × k log k) para n strings. - Contagem: O(k) por string e O(n × k) para n strings. A melhoria apareceu quando percebi que a ordenação fazia um trabalho que o problema não exigia. A mudança principal é de O(k log k) para O(k) por string. ## A primeira solução não estava errada Ela resolve o problema e, dependendo do contexto, poderia ser suficiente. O hábito que eu queria recuperar era continuar pensando depois que o código começa a funcionar. Encontrar uma solução, voltar ao problema e perguntar: que trabalho meu algoritmo está fazendo sem precisar? Não estou fazendo esses exercícios porque LeetCode representa todo o trabalho de engenharia de software, nem para disputar a solução mais sofisticada. Estou fazendo porque percebi que usar IA todos os dias reduziu a quantidade de vezes em que preciso insistir sozinho em um problema. Quero reservar espaço para exercitar isso novamente: ler, tentar, errar, revisar a abordagem e só depois comparar caminhos. --- *Série Desenferrujando a lógica #01 — Group Anagrams. Adaptado do carousel autoral; problema em [leetcode.com/problems/group-anagrams](https://leetcode.com/problems/group-anagrams/).* Chave por sort e mapa Cada string vira chave ordenada (eat → aet). O mapa agrupa anagramas sob a mesma chave sem comparar cada par. Contagem [26]uint8 vs sort Vetor de frequências a–z vira chave. Contagem custa O(k) por string; sort custava O(k log k). Total: de O(n × k log k) para O(n × k). --- # Pare de ser refém das dependências. Diga olá ao Design Pattern Adapter URL: https://imrafaeldev.site/artigos/design-patterns-adapter > Com o Adapter, o serviço depende de um protocolo e os adapters traduzem MySQL, PostgreSQL ou mocks — sem acoplar a regra de negócio ao driver. - [Início](/) - [Artigos](/artigos/) - Pare de ser refém das dependências. Diga olá ao Design Pattern Adapter ## Pare de ser refém das dependências. Diga olá ao Design Pattern Adapter Migrar de MySQL para PostgreSQL não precisa reescrever o serviço. O Adapter isola o plugin atrás de um contrato que a regra de negócio entende. 26 de abril de 2022 - [Arquitetura](/artigos/?topic=arquitetura) Com o padrão Adapter, isolamos a regra de negócio da dependência concreta. ## Cenário inicial Temos um backend com CRUD simples de usuário: criar, editar, recuperar e deletar por endpoints na API. Os dados vão para um banco qualquer — digamos MySQL — e a estrutura nasce como **Controller → Service → Database**. Até aqui, o time está confortável. Até alguém decidir: na semana que vem migramos de MySQL para PostgreSQL. A partir daí a casa cai para tecnologia. Além de reestruturar o banco, o time precisa caçar referências ao MySQL: inserções, conexão, queries espalhadas. Na maioria das vezes isso atrasa entrega, reduz qualidade, pula testes e introduz bugs. Seria melhor diminuir a dependência entre a regra de negócio e quem executa a operação específica — no caso, o banco. O Adapter ajuda a construir o sistema assim. ## Padrão Adapter Temos um plugin, library, module ou serviço de terceiros que faz algo que queremos na regra de negócio — aqui, persistir dados. O caminho: - Definir uma interface com o contrato do que precisamos. - Expor só métodos que fazem sentido no contexto (SOLID). - Implementar a interface em classes que adaptam o código de terceiros. Criamos CreateDatabaseCustomerProtocol com um método create que recebe CustomerInputEntity e retorna SuccessfulEntityCreation: interface CustomerInputEntity { name: string; email: string; birthDate: Date; } interface SuccessfulEntityCreation { readonly id: number; readonly name: string; readonly email: string; readonly birthDate: Date; } interface CreateDatabaseCustomerProtocol { createCustomerOnDatabase( customer: CustomerInputEntity, ): SuccessfulEntityCreation; } Em vez de **Controller → Service → Database**, passamos a **Controller → Service → Protocols → Plugin**. O serviço perde o conhecimento de como o CRUD chega ao banco. Ele é composto pelos protocolos; a implementação concreta entra em tempo de execução — por injeção de dependência. Enquanto usamos PostgreSQL, implementamos os protocolos nos adapters (ou connectors). CreateDatabaseCustomerProtocol pode ser implementado por CreateDatabaseCustomerPostgresqlAdapter, CreateDatabaseCustomerMysqlAdapter, CreateDatabaseCustomerMongoDBAdapter ou CreateDatabaseCustomerMockedAdapter. O serviço fica assim: class CustomerService { constructor( private readonly createCustomer: CreateDatabaseCustomerProtocol, ) {} public register(customer: CustomerInputEntity): SuccessfulEntityCreation { return this.createCustomer.createCustomerOnDatabase(customer); } } Para o serviço, tanto faz se o banco devolve JSON, XML ou outro formato — o adapter traduz para o contrato que a regra de negócio espera. ## Vantagens - **Manutenção:** qualquer plugin pode ser substituído sem reescrever o serviço. - **Testes:** para testar só a regra de negócio, injete um adapter mock que implementa o mesmo protocolo. - **Código limpo:** responsabilidades separadas; a regra de negócio não carrega detalhes do driver. ## Próximo passo Pegue um frontend com dezenas de bibliotecas e identifique o que você realmente usa. Escolha uma funcionalidade — converter real em dólar, por exemplo. Descreva o contrato (entrada e saída) e implemente um adapter em cima da biblioteca que hoje faz isso. Repita onde a dependência incomoda. ## Relação com outros padrões No artigo [Design Patterns: Strategy](/artigos/design-patterns-strategy/), o foco é trocar algoritmos atrás de um contrato. O Adapter isola dependências externas atrás de uma interface própria. Os dois se complementam: Strategy varia comportamento; Adapter traduz o mundo de fora. --- *Publicado originalmente no [LinkedIn](https://www.linkedin.com/pulse/pare-de-ser-ref%C3%A9m-das-depend%C3%AAncias-diga-bem-vindo-ao-design-rafael/) em 26 de abril de 2022.* Adapter: protocolo e implementações CustomerService usa CreateDatabaseCustomerProtocol. MysqlAdapter e PostgresqlAdapter implementam o contrato e traduzem para cada banco. Camadas: antes e depois Acoplamento direto ao MySQL dificulta migração. Com protocolo e adapter, troca-se o plugin sem reescrever o serviço. --- # Design Patterns: Strategy URL: https://imrafaeldev.site/artigos/design-patterns-strategy > Como o padrão Strategy encapsula algoritmos intercambiáveis e evita cadeias frágeis de if/else, com um exemplo de calculadora em TypeScript. - [Início](/) - [Artigos](/artigos/) - Design Patterns: Strategy ## Design Patterns: Strategy Cadeias de if/else crescem e ficam frágeis. O Strategy isola cada algoritmo atrás de um contrato. 5 de maio de 2022 - [Arquitetura](/artigos/?topic=arquitetura) Cadeias de if/else crescem e ficam frágeis. O padrão Strategy encapsula cada algoritmo em sua própria classe e permite trocar implementações em tempo de execução sem alterar o código que as consome. ## O problema: if/else infinito Uma calculadora com soma, subtração, multiplicação e divisão costuma nascer como uma classe DefaultCalculator: métodos privados por operação e uma função pública que escolhe qual invocar com switch ou cadeia de if/else. class DefaultCalculator { public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; switch (operator) { case "*": return firstOperand * secondOperand; case "+": return firstOperand + secondOperand; case "-": return firstOperand - secondOperand; case "/": return firstOperand / secondOperand; case "**": return firstOperand ** secondOperand; case "%": return firstOperand % secondOperand; default: throw new Error("Operator not found!"); } } } O problema aparece quando a calculadora precisa cobrir mais operações binárias entre inteiros: percentual, exponenciação, módulo, shift de bits. Cada funcionalidade nova altera a implementação original, sobe o acoplamento e encarece a manutenção. ## O que é o padrão Strategy? O padrão define a funcionalidade por meio de um contrato (interface), implementado conforme o contexto. A interface define a operação; as implementações concretas definem sua execução. O código consumidor depende da abstração. Cada estratégia fica isolada em sua própria classe. Novos comportamentos entram sem alterar o código existente, alinhado ao Open/Closed Principle. ## Definindo o contrato O primeiro passo é a interface do contrato da estratégia. Na calculadora, algo que receba dois números e retorne o resultado: interface BinaryOperationParameters { firstOperand: number; secondOperand: number; operator: string; } type Result = number; interface BinaryOperationStrategy { calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result; } ## Implementando estratégias concretas Cada operação matemática vira uma classe que implementa BinaryOperationStrategy e executa uma única operação. Soma e divisão: class Sum implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; return firstOperand + secondOperand; } } class Division implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; if (secondOperand === 0) { throw new Error("Division by zero is not allowed!"); } return firstOperand / secondOperand; } } ## O Context e a Factory Para amarrar as estratégias, entram um Context e uma Factory (ou Analyzer). Em ContextAnalyzer, um método avalia o operador e retorna a Strategy correta: class ContextAnalyzer { public getInstance(operator: string): BinaryOperationStrategy { switch (operator) { case "*": return new Multiplication(); case "+": return new Sum(); case "-": return new Subtraction(); case "/": return new Division(); case "%": return new Percent(); case "**": return new Pow(); default: throw new Error("Operator not found!"); } } } O contexto recebe e executa a estratégia. Ele conhece apenas o contrato, não a implementação concreta. A antiga DefaultCalculator passa a receber esse ContextAnalyzer por injeção: class Calculator { constructor(private readonly contextAnalyzer: ContextAnalyzer) {} /** * A implementação de calculate em Calculator não muda a cada operação. * O que cresce é o ContextAnalyzer, que adiciona um case por operação nova. */ public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; return this.contextAnalyzer .getInstance(operator) .calculate({ firstOperand, secondOperand }); } } ## Por que usar o Strategy? O Strategy ajuda em legado com várias regras de negócio, cada uma representada por um if e uma implementação extensa. A parte comum fica no contrato; cada variação de regra fica em sua própria classe; um analisador de contexto (resolver/factory) escolhe a estratégia. A cada requisição, o código avalia o contexto da operação e seleciona a implementação correspondente ao contrato. ## Relação com outros padrões - Adapter: o Strategy varia comportamento; o Adapter isola dependências externas atrás de uma interface própria. - SOLID (OCP): o Strategy é uma forma de aplicar o Open/Closed Principle. Strategy: contrato, seleção e concretas Calculator depende do contrato BinaryOperationStrategy. ContextAnalyzer escolhe a concreta (ex.: Sum) pelo operador; Sum, Division e Pow implementam o mesmo contrato. --- # Goroutines vs Event Loop: a comparação errada entre dois modelos de concorrência URL: https://imrafaeldev.site/artigos/goroutines-vs-event-loop > Concorrência não é paralelismo. Quando o Event Loop do Node.js basta para I/O e quando goroutines em Go encaixam melhor em carga CPU bound. - [Início](/) - [Artigos](/artigos/) - Goroutines vs Event Loop: a comparação errada entre dois modelos de concorrência ## Goroutines vs Event Loop: a comparação errada entre dois modelos de concorrência Node.js com Event Loop e Go com goroutines não resolvem o mesmo problema do mesmo jeito. O erro comum é confundir concorrência com paralelismo. 24 de junho de 2026 - [Trade-offs](/artigos/?topic=trade-offs) - [Performance](/artigos/?topic=performance) A tese é simples: Node.js com Event Loop e Go com goroutines não resolvem o mesmo tipo de problema do mesmo jeito. A comparação fica ruim quando a gente trata os dois como concorrentes diretos em qualquer cenário. Na prática, o erro mais comum é confundir concorrência com paralelismo. Concorrência é organizar várias tarefas que podem estar em andamento ao mesmo tempo. Paralelismo é executar trabalho de fato ao mesmo tempo, usando múltiplos núcleos de CPU. Essa diferença parece acadêmica até aparecer em produção. O Event Loop do Node.js é muito bom quando o gargalo está em espera: API externa, banco de dados, WebSocket, input de usuário, filas e eventos. Enquanto uma operação aguarda resposta, o loop continua atendendo outras tarefas. É o dono da bodega no balcão: ele não para porque pediu a alguém para buscar a rapadura no estoque. Goroutines, por outro lado, começam a ficar mais interessantes quando o trabalho é CPU bound, divisível e pode aproveitar múltiplos núcleos com controle explícito de concorrência. Elas são unidades leves de execução gerenciadas pelo runtime do Go. Com elas, dá para quebrar uma tarefa em partes menores, distribuir a execução e sincronizar o resultado no final. É mais parecido com uma barraca cheia no São João de Caruaru: uma pessoa assa o milho, outra mexe a canjica, outra corta o bolo de rolo. O trabalho avança ao mesmo tempo, com cada pessoa cuidando de uma parte. ## O ponto onde o Node.js começa a sofrer Eu vi isso de forma bem concreta em um processo de cálculo de comissão em uma casa de apostas. A aplicação lidava com milhões de apostas por dia, e parte do fluxo envolvia calcular comissão sobre vários lotes de apostas. No começo, o processo em Node.js funcionava. Sequencialmente era correto, mas lento. Quando tentei paralelizar com a lógica comum de várias tarefas ao mesmo tempo, o limite apareceu: o gargalo era CPU. Não era só esperar banco, API ou evento externo. Era cálculo em cima de um buffer grande de apostas. Esse é o tipo de cenário em que Promise.all pode enganar. Ele passa a sensação de paralelismo, mas não transforma automaticamente trabalho pesado de CPU em execução paralela real. Se as tarefas são computação intensa e rodam no mesmo thread principal, o Event Loop fica ocupado. O resultado pode ser pior do que o esperado: bloqueio do loop, aumento de latência, pior responsividade e maior pressão sobre CPU e memória. O problema não era Node.js ser ruim. O problema era usar o modelo padrão do Node para uma carga que exigia outro tipo de execução. ## Onde Go entrou melhor A solução foi reescrever esse processo em Go usando goroutines. A ideia era dividir o cálculo em chunks menores, processar esses pedaços em paralelo e sincronizar apenas no final. Esse desenho encaixava melhor no problema porque o trabalho era CPU bound e podia ser dividido. Em vez de um fluxo centralizado tentando coordenar várias operações pesadas, o processamento passou a ser distribuído em unidades menores de execução. Com um worker pool, por exemplo, dá para controlar o número de goroutines, limitar o fan-out, usar melhor os cores disponíveis e evitar que o sistema dispare trabalho sem limite. O ganho apareceu. O tempo total caiu cerca de 25%. O processo que ficava na casa dos 30 segundos passou a rodar em algo próximo de 22,5 segundos. Também houve melhor uso de CPU. Mas a parte importante da história não é “Go resolveu”. A parte importante é que Go resolveu um lado do problema e revelou outro. ## O gargalo pode mudar de lugar A primeira dificuldade foi garantir que o resultado em Go fosse igual ao resultado em Node.js. Isso é menos glamouroso do que falar sobre concorrência, mas é o que separa otimização real de regressão mascarada. Se o cálculo fica mais rápido e muda o resultado financeiro, a melhoria não vale nada. Depois disso, o problema principal virou a divisão dos chunks. A estratégia inicial consumia memória demais. Em testes locais, com datasets menores, o crescimento proporcional chegou perto de 15% em alguns momentos. O incômodo vinha de uma expectativa errada: eu achei que mudar para Go automaticamente resolveria o problema. Na prática, eu só tinha movido o gargalo de lugar. Antes o limite estava mais claro na CPU. Depois, a estratégia de particionamento começou a pressionar memória. Isso pode acontecer por vários motivos: cópias desnecessárias, buffers grandes, slices mantendo referência para arrays maiores, filas internas grandes demais ou excesso de trabalho sendo preparado antes de ser processado. No resultado final, a memória ainda cresceu cerca de 5%. Nesse caso, o ganho de tempo e CPU compensou a perda. Mas isso não é uma regra universal. Se a carga de produção fosse muito maior, ou se o serviço estivesse rodando com margem pequena de memória, essa troca poderia deixar de ser aceitável. Paralelismo custa coordenação, alocação, sincronização e observabilidade. Não existe execução paralela grátis. ## Quando eu manteria Node.js Eu manteria Node.js sem incômodo para orquestração de I/O: comunicação por WebSocket, chamadas para várias APIs, consultas em banco, disparo de eventos, integração entre serviços e fluxos onde o tempo morto está na espera. Nesses casos, o Event Loop é uma excelente escolha. Ele permite alto volume de operações concorrentes sem criar uma thread por requisição. Para aplicações orientadas a evento, isso é simples, produtivo e fácil de encaixar no ecossistema JavaScript. O erro é tentar empurrar esse mesmo modelo para um cálculo pesado e achar que concorrência de I/O vira paralelismo de CPU. Não vira. ## Quando eu olharia para Go Eu começaria a olhar para Go quando a tarefa fosse claramente CPU bound: cálculo em alto volume de dados, processamento de imagem em lote, agregações pesadas, compressão, transformação grande de buffers, simulações ou qualquer rotina em que a máquina passa mais tempo calculando do que esperando resposta externa. Nesse tipo de cenário, goroutines com worker pool dão um controle melhor sobre uso de CPU. Também deixam mais explícita a separação entre unidades de trabalho, sincronização e coleta de resultado. Mas Go também cobra preço. É preciso pensar em granularidade dos chunks, consumo de memória, cancelamento, tratamento de erro, backpressure, limites de workers, contenção e consistência do resultado. Se a divisão do trabalho for ingênua, o ganho de CPU pode vir acompanhado de estouro de memória ou complexidade desnecessária. ## Contraargumento: Node.js também tem worker threads Existe um contraargumento justo: Node.js não está limitado ao Event Loop para tudo. Worker threads existem justamente para executar trabalho pesado fora do thread principal. Também há estratégias com filas, processos separados, serviços auxiliares e native addons. Então a comparação honesta não é “Node.js não consegue”. Consegue. A questão é custo de implementação, maturidade da equipe, observabilidade, integração com o sistema existente e quanto esforço vale a pena investir para manter aquele processamento dentro do ecossistema Node. Em alguns times, usar worker threads pode ser suficiente e mais barato do que introduzir Go. Em outros, separar o processamento CPU bound em um serviço Go pode ser mais simples de operar e escalar. A decisão não deveria nascer de preferência por linguagem. Deveria nascer da natureza da carga. ## A regra prática que ficou Depois desse caso, a regra que ficou para mim é esta: se o problema é esperar muita coisa ao mesmo tempo, Node.js com Event Loop tende a orquestrar muito --- # Artigos URL: https://imrafaeldev.site/artigos > Padrões e decisões de engenharia em prosa técnica, com código. - [Início](/) - Artigos ## Artigos Padrões e decisões de engenharia em prosa técnica, com código. TodosPerformanceArquiteturaTrade-offsMensageria - ## [Intensivão Golang: concorrência, resiliência e sistemas distribuídos em 30 minutos](/artigos/intensivao-go/) 22 de setembro de 2026 [Arquitetura](/artigos/?topic=arquitetura) - [Performance](/artigos/?topic=performance) - [Mensageria](/artigos/?topic=mensageria) Um roteiro de 30 minutos para reativar Go aplicado a serviços de produção, ingestão de telemetria e sistemas distribuídos. - ## [Intensivão Golang Avançado: aprofundamento em concorrência, arquitetura e trade-offs](/artigos/intensivao-golang-avancado/) 22 de setembro de 2026 [Arquitetura](/artigos/?topic=arquitetura) - [Performance](/artigos/?topic=performance) - [Trade-offs](/artigos/?topic=trade-offs) O passo seguinte ao roteiro rápido de Go, com aprofundamento guiado em concorrência, arquitetura e trade-offs. - ## [Desenferrujando a lógica #02: Container With Most Water](/artigos/desenferrujando-logica-container-with-most-water/) 15 de setembro de 2026 [Performance](/artigos/?topic=performance) - [Trade-offs](/artigos/?topic=trade-offs) Eu tentei escolher o próximo passo olhando apenas para os vizinhos. Funcionou em alguns casos, mas o problema pedia uma visão mais ampla. - ## [Desenferrujando a lógica #01: Group Anagrams](/artigos/desenferrujando-logica-group-anagrams/) 17 de agosto de 2026 [Trade-offs](/artigos/?topic=trade-offs) - [Performance](/artigos/?topic=performance) Eu não tinha parado de escrever código. O que mudou foi terceirizar partes do raciocínio. Group Anagrams foi o exercício para recuperar o hábito. - ## [A maioria dos problemas de performance de backend começa perto dos dados](/artigos/backend-performance-perto-dos-dados/) 16 de julho de 2026 [Performance](/artigos/?topic=performance) - [Arquitetura](/artigos/?topic=arquitetura) API lenta e a reunião já enche de soluções. Eu começo perto dos dados — não por culpar o banco, mas porque essa verificação costuma dar sinal rápido. - ## [Goroutines vs Event Loop: a comparação errada entre dois modelos de concorrência](/artigos/goroutines-vs-event-loop/) 24 de junho de 2026 [Trade-offs](/artigos/?topic=trade-offs) - [Performance](/artigos/?topic=performance) Node.js com Event Loop e Go com goroutines não resolvem o mesmo problema do mesmo jeito. O erro comum é confundir concorrência com paralelismo. - ## [TypeScript Clean Architecture: Core, Adapters e Infra](/artigos/typescript-cleanarch/) 15 de março de 2023 [Arquitetura](/artigos/?topic=arquitetura) Arquitetura ruim trava manutenção, testes e mudança. Esta derivação da Clean Architecture para backend TypeScript separa Core, Adapters e Infra — com dependências apontando para dentro. - ## [Design Patterns: Strategy](/artigos/design-patterns-strategy/) 5 de maio de 2022 [Arquitetura](/artigos/?topic=arquitetura) Cadeias de if/else crescem e ficam frágeis. O Strategy isola cada algoritmo atrás de um contrato. - ## [Pare de ser refém das dependências. Diga olá ao Design Pattern Adapter](/artigos/design-patterns-adapter/) 26 de abril de 2022 [Arquitetura](/artigos/?topic=arquitetura) Migrar de MySQL para PostgreSQL não precisa reescrever o serviço. O Adapter isola o plugin atrás de um contrato que a regra de negócio entende. --- # Intensivão Golang: concorrência, resiliência e sistemas distribuídos em 30 minutos URL: https://imrafaeldev.site/artigos/intensivao-go > Revisão prática de Go para backend: goroutines, context, backpressure, idempotência, Kubernetes, observabilidade e performance. - [Início](/) - [Artigos](/artigos/) - Intensivão Golang: concorrência, resiliência e sistemas distribuídos em 30 minutos ## Intensivão Golang: concorrência, resiliência e sistemas distribuídos em 30 minutos Um roteiro de 30 minutos para reativar Go aplicado a serviços de produção, ingestão de telemetria e sistemas distribuídos. 22 de setembro de 2026 - [Arquitetura](/artigos/?topic=arquitetura) - [Performance](/artigos/?topic=performance) - [Mensageria](/artigos/?topic=mensageria) Este roteiro serve para quem já trabalha com backend e quer reativar Go para uma conversa técnica ou um serviço de produção. O foco está nas decisões que mantêm um sistema previsível sob carga: concorrência limitada, cancelamento, filas finitas, idempotência e observabilidade. O recorte usa Go 1.26. Não é uma introdução à linguagem. Passe rápido pelos fundamentos e retenha os pontos que mudam o desenho de um consumer, uma API ou um pipeline de telemetria. ## Roteiro de 30 minutos Tempo Bloco Prioridade 0-4 min Tipos, structs, interfaces e erros Revisão rápida 4-10 min Goroutines, channels, select e contexto Alta 10-17 min Limites de concorrência e backpressure Máxima 17-23 min Pipeline IoT resiliente Máxima 23-26 min Runtime, memória e profiling Alta 26-30 min Arquitetura e perguntas de entrevista Máxima ## Fundamentos que aparecem em produção Go favorece composição, contratos pequenos e fluxo explícito. Não há herança de classes nem exceções como mecanismo normal de controle. Um tipo simples pode carregar sua própria validação: package telemetry import ( "errors" "fmt" "time" ) var ErrOutOfRange = errors.New("reading out of range") type Reading struct { DeviceID string `json:"device_id"` Sequence uint64 `json:"sequence"` ObservedAt time.Time `json:"observed_at"` Value float64 `json:"value"` } func (r Reading) Validate() error { if r.DeviceID == "" { return errors.New("device_id is required") } if r.Value < -100 || r.Value > 250 { return fmt.Errorf("%w: %.2f", ErrOutOfRange, r.Value) } return nil } Alguns detalhes evitam erros silenciosos: - O zero value costuma ser utilizável. Prefira tipos cujo estado inicial seja válido quando isso não esconder uma regra de negócio. - Slice é uma visão sobre um array. Cópias podem compartilhar o mesmo backing array; append pode reutilizá-lo ou alocar outro. - map não tem ordem de iteração e não suporta leitura e escrita concorrentes sem sincronização. - string contém bytes imutáveis, normalmente UTF-8. len conta bytes; range decodifica runes. - defer executa em LIFO, mas avalia os argumentos quando é registrado. Interfaces são satisfeitas implicitamente. Defina a interface pequena no pacote que a consome, em vez de exportar um contrato grande ao lado da implementação. Erros são valores: acrescente contexto com %w e inspecione a causa com errors.Is ou errors.As. if err := reading.Validate(); err != nil { if errors.Is(err, ErrOutOfRange) { return sendToDLQ(reading, err) } return fmt.Errorf("validate reading: %w", err) } panic fica para invariantes quebradas ou falha irrecuperável de inicialização. recover só alcança um panic na mesma goroutine e pertence a fronteiras controladas, como middleware. ## Goroutines, channels e cancelamento Uma goroutine não é uma thread dedicada. O runtime a agenda sobre threads do sistema operacional. Iniciar uma goroutine sem saber quem a cancela ou espera cria risco de leak. Channels transportam trabalho ou propriedade. Um mutex protege estado compartilhado. Um buffer apenas absorve uma diferença temporária de velocidade; não cria capacidade infinita. jobs := make(chan Reading, 128) go func() { defer close(jobs) // quem produz fecha for _, reading := range batch { jobs <- reading } }() for reading := range jobs { if err := process(reading); err != nil { // tratar ou registrar o erro da mensagem } } O produtor fecha o channel quando não haverá novo envio. Enviar em channel fechado ou fechá-lo duas vezes causa panic. Receber de um channel fechado retorna o zero value e ok == false. Um channel nil bloqueia para sempre; dentro de select, ele desabilita o caso. select combina envio, cancelamento e política de saturação. Sem default, a operação espera espaço ou cancelamento. Com default, ela rejeita imediatamente quando a fila está cheia: func enqueue(ctx context.Context, jobs chan<- Reading, reading Reading) error { select { case jobs <- reading: return nil case <-ctx.Done(): return context.Cause(ctx) } } context.Context carrega cancelamento, deadline e metadados estritamente ligados à requisição. Receba-o como primeiro argumento, propague-o, chame todo cancel retornado e não guarde contexto em struct. Cancelar não encerra uma goroutine à força: os loops e operações bloqueantes precisam observar ctx.Done(). ## Concorrência limitada antes da pressão de memória Uma goroutine por mensagem parece barata até um downstream ficar lento. A fila cresce, o heap cresce, o GC trabalha mais e o processo pode cair antes de a CPU parecer saturada. Para tarefas independentes que falham juntas, errgroup oferece espera, propagação do primeiro erro e cancelamento compartilhado. SetLimit estabelece o teto de concorrência. func ProcessBatch(ctx context.Context, batch []Reading) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(16) for _, reading := range batch { reading := reading g.Go(func() error { if err := processOne(ctx, reading); err != nil { return fmt.Errorf("device %s: %w", reading.DeviceID, err) } return nil }) } return g.Wait() } Classifique o erro antes de decidir o que fazer. Um banco indisponível pode justificar cancelar o lote. Um payload inválido, duplicado ou fora do schema deve ir para quarentena ou DLQ, sem derrubar o consumer inteiro. Escolha a primitiva pela propriedade que precisa preservar: Necessidade Primitiva Contador ou flag independente atomic tipado Invariante entre vários campos sync.Mutex Leitura frequente e escrita curta sync.RWMutex, depois de medir Transferir trabalho ou ownership channel Inicialização única sync.Once Esperar tarefas sem erro sync.WaitGroup Esperar tarefas com erro e cancelamento errgroup Não copie mutex depois do primeiro uso e não mantenha lock durante I/O remoto. RWMutex não é uma melhoria automática para uma seção crítica pequena. Backpressure é uma decisão de produto e operação. Se a ingestão recebe 50 mil mensagens por segundo e a persistência confirma 20 mil, acumular o restante em memória apenas muda o incidente de lugar. Política Consequência Bloquear produtor Aumenta latência e preserva dados quando o protocolo aceita desacelerar Rejeitar com erro Exige retry e idempotência no cliente Pausar consumo ou ACK Mantém backlog no broker durável Descartar dados antigos Preserva frescor quando histórico não importa Agregar ou downsample Reduz resolução para aliviar a carga Persistir em disco Evita perda, com custo operacional adicional Defina tamanho de buffer, métrica de ocupação, timeout e ação de saturação. Buffer sem política não é estratégia de capacidade. ## Pipeline IoT que tolera reentrega Uma separação comum é: dispositivo -> MQTT/broker -> ingestão Go -> stream -> processadores -> armazenamento \-> DLQ \-> estado atual MQTT atende bem à borda e às conexões dos dispositivos. Um stream como Kafka atende retenção, replay e particionamento interno. gRPC é RPC interno tipado; WebSocket atende atualização de dashboards. Nenhum deles substitui os outros automaticamente. Fan-out de workers quebra ordem global. Em telemetria, o requisito costuma ser ordem por dispositivo. Particione por uma chave estável, como hash(device_id) % N, e processe cada partição de forma sequencial. Guarde observed_at, ingested_at, sequence, event_id e, quando existir, boot_id. O relógio do dispositivo pode estar errado ou reiniciar. Projete a cadeia para entrega *at least once*. Um fluxo seguro recebe o evento, valida envelope e versão, verifica a chave de idempotência, grava --- # Intensivão Golang Avançado: aprofundamento em concorrência, arquitetura e trade-offs URL: https://imrafaeldev.site/artigos/intensivao-golang-avancado > Aprofundamento do roteiro de 30 minutos de Go: limites de concorrência, desenho de consumers, idempotência e trade-offs de produção. - [Início](/) - [Artigos](/artigos/) - Intensivão Golang Avançado: aprofundamento em concorrência, arquitetura e trade-offs ## Intensivão Golang Avançado: aprofundamento em concorrência, arquitetura e trade-offs O passo seguinte ao roteiro rápido de Go, com aprofundamento guiado em concorrência, arquitetura e trade-offs. 22 de setembro de 2026 - [Arquitetura](/artigos/?topic=arquitetura) - [Performance](/artigos/?topic=performance) - [Trade-offs](/artigos/?topic=trade-offs) Este guia é o aprofundamento do roteiro de 30 minutos de Go. Ele parte dos mesmos fundamentos — goroutines, channels, contexto, backpressure e idempotência — para discutir com mais calma quando cada decisão de desenho se aplica. Se você ainda não passou pela revisão rápida, comece pelo [roteiro de 30 minutos](/artigos/intensivao-go/) e volte aqui para aprofundar cada bloco. ## Como usar este guia Avance na ordem proposta: cada seção isola uma decisão de desenho, descreve o mecanismo, a falha que ele trata, um exemplo autocontido e o critério para escolher outra abordagem. Todos os cenários abaixo são didáticos e hipotéticos — servem para treinar leitura de trade-offs, não descrevem sistemas reais. Leia com um editor aberto e adapte os snippets ao seu próprio exercício antes de levar qualquer padrão para um sistema real. ## 1. Modelo de execução: goroutines, scheduler e GOMAXPROCS **Mecanismo.** Goroutines são unidades leves de execução multiplexadas pelo scheduler do runtime sobre threads do sistema operacional. GOMAXPROCS define quantas threads podem executar código Go simultaneamente; por padrão, acompanha o número de CPUs disponíveis. Desde o Go 1.14, as goroutines são assincronamente preemptíveis: o scheduler pode interrompê-las mesmo em loops apertados sem pontos explícitos de cooperação, distribuindo trabalho sem que cada tarefa exija uma thread dedicada. **Falha ou limite que ele trata.** O modelo evita o custo de uma thread por tarefa concorrente e reduz troca de contexto do sistema operacional. O limite aparece quando se confunde concorrência com paralelismo: criar milhares de goroutines bloqueadas em I/O lento é barato, mas criar milhares de goroutines em loop apertado de CPU com GOMAXPROCS baixo apenas serializa o trabalho e aumenta pressão sobre o escalonador e o coletor de lixo. **Exemplo de aplicação.** Cenário didático: processar uma lista de itens independentes em paralelo, limitada ao número de CPUs. package main import ( "fmt" "runtime" "sync" ) func process(item int) int { // Simula transformação pura de CPU. return item * item } func main() { items := []int{1, 2, 3, 4, 5, 6, 7, 8} results := make([]int, len(items)) numWorkers := runtime.GOMAXPROCS(0) jobs := make(chan int) var wg sync.WaitGroup for w := 0; w < numWorkers; w++ { wg.Add(1) go func() { defer wg.Done() for index := range jobs { results[index] = process(items[index]) } }() } for i := range items { jobs <- i } close(jobs) wg.Wait() fmt.Println(results) } **Quando escolher outra abordagem.** Mantenha o valor default de GOMAXPROCS; se considerar sobrescrevê-lo, meça antes e depois em benchmarks controlados. Para tarefas puramente sequenciais, dependentes entre si ou com overhead de coordenação maior que o ganho, o laço simples sem goroutines é mais legível e mais rápido. Para paralelismo de dados em lote com cancelamento e limite de erro, prefira errgroup ou um pool com semáforo em vez de disparar goroutines sem controle. ## 2. Ownership e cancelamento com context **Mecanismo.** context.Context propaga cancelamento, deadline e valores de escopo de requisição ao longo de uma cadeia de chamadas. O dono do contexto (normalmente a borda de entrada: handler HTTP, consumidor de fila, função main) cria um contexto cancelável ou com timeout; as funções internas apenas observam <-ctx.Done() e retornam ctx.Err(). Contexto é imutável: WithCancel, WithTimeout e WithValue derivam um filho sem alterar o pai. **Falha ou limite que ele trata.** Sem ownership claro, goroutines órfãs continuam trabalhando depois que o cliente desistiu, o deploy desligou ou o timeout estourou — desperdiçando CPU, conexões e memória. O limite do mecanismo: contexto não cancela código por força; se a função ignorar ctx.Done() ou bloquear em operação sem suporte a contexto, o cancelamento nunca acontece. **Exemplo de aplicação.** Cenário didático: uma busca com timeout que abandona o trabalho lento. package main import ( "context" "fmt" "time" ) func fetch(ctx context.Context, id int) (string, error) { timer := time.NewTimer(2 * time.Second) defer timer.Stop() select { case <-timer.C: return fmt.Sprintf("item-%d", id), nil case <-ctx.Done(): return "", ctx.Err() } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() result, err := fetch(ctx, 42) if err != nil { fmt.Println("cancelado:", err) return } fmt.Println(result) } **Quando escolher outra abordagem.** Use valores de contexto apenas para dados de escopo de requisição (identificador de correlação, credenciais de chamada). Nunca use contexto para parâmetros obrigatórios da função nem para estado mutável compartilhado — passe argumentos explícitos. Se o cancelamento precisa interromper computação que não observa contexto (loop apertado de CPU), verifique ctx.Done() manualmente a cada iteração ou reestruture o trabalho em etapas interrompíveis. ## 3. Channels e sincronização: quando o mutex é melhor **Mecanismo.** Channels transferem *posse de dados* entre goroutines e sincronizam remetente e receptor; sync.Mutex (e sync.RWMutex) protegem *acesso a estado compartilhado*. A regra prática: use channels para orquestrar (sinalizar conclusão, distribuir tarefas, aplicar backpressure) e mutex para guardar invariantes de uma estrutura acessada por várias goroutines (contadores, caches, mapas). **Falha ou limite que ele trata.** Channels evitam condição de corrida por construção quando o dado atravessa o canal em vez de ser compartilhado. O limite: modelar todo estado compartilhado com uma goroutine “dona” e canais de pedido/resposta adiciona latência, complexidade e risco de deadlock quando um simples mutex resolveria. Inversamente, proteger um pipeline inteiro com um único mutex gigante serializa trabalho que poderia fluir em paralelo. **Exemplo de aplicação.** Cenário didático: o mesmo contador implementado das duas formas para comparar. package main import ( "fmt" "sync" ) // Com mutex: direto para estado compartilhado simples. type Counter struct { mu sync.Mutex n int } func (c *Counter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.n++ } // Com channel: a goroutine dona centraliza as atualizações. func runCounterOwner(increments int) int { inc := make(chan struct{}) done := make(chan int) go func() { total := 0 for range inc { total++ } done <- total }() var wg sync.WaitGroup for i := 0; i < increments; i++ { wg.Add(1) go func() { defer wg.Done() inc <- struct{}{} }() } wg.Wait() close(inc) return <-done } func main() { var c Counter var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() c.Inc() }() } wg.Wait() fmt.Println("mutex:", c.n) fmt.Println("owner:", runCounterOwner(100)) } **Quando escolher outra abordagem.** Prefira sync.Mutex/sync.RWMutex para proteger mapas, contadores e caches com acesso concorrente simples; prefira sync.Map apenas quando houver padrão comprovado de muitas leituras e poucas escritas com chaves disjuntas. Prefira channels quando precisar de fila, fan-out/fan-in, timeout via select ou backpressure natural com canal com buffer. Evite expor canais internos como API de uma estrutura com estado pequeno — o mutex mantém a interface síncrona e mais fácil de testar. ## 4. Concorrência limitada e backpressure **Mecanismo.** Concorrência limitada impõe um teto de trabalhos simultâneos com semáforo (canal com buffer de vagas), pool de workers ou errgroup.Group com limite. Backpressure é o efeito: --- # TypeScript Clean Architecture: Core, Adapters e Infra URL: https://imrafaeldev.site/artigos/typescript-cleanarch > Derivação da Clean Architecture para backend TypeScript: Core com usecases e protocols, Adapters bidirecionais e Infra NestJS com injeção de dependências. - [Início](/) - [Artigos](/artigos/) - TypeScript Clean Architecture: Core, Adapters e Infra ## TypeScript Clean Architecture: Core, Adapters e Infra Arquitetura ruim trava manutenção, testes e mudança. Esta derivação da Clean Architecture para backend TypeScript separa Core, Adapters e Infra — com dependências apontando para dentro. 15 de março de 2023 - [Arquitetura](/artigos/?topic=arquitetura) Desenvolvimento de software muda o tempo todo. Arquitetura fraca vira manutenção cara, feature lenta, teste difícil e bug difícil de isolar. Vale investir em uma estrutura que suporte evolução sem reescrever o sistema a cada pressão do negócio. ## Um pouco de história Clean Architecture é o nome que Robert C. Martin (Uncle Bob) deu, em 2012, no livro *Clean Architecture: A Craftsman’s Guide to Software Structure and Design*. A proposta foge da rigidez de arquiteturas acopladas a framework e banco: o núcleo fica estável; detalhes externos mudam. A ideia bebe de DDD, SOLID, Onion Architecture e Hexagonal Architecture. ## Proposta geral Este artigo descreve a Clean Architecture e uma derivação prática para backends em TypeScript: três camadas — **Core**, **Adapters** e **Infra**. - **Core** — regra de negócio e entidades do domínio. Camada mais interna. - **Infra** — conexões externas: repositórios concretos, controllers REST, módulos de DI, boilerplate de framework. - **Adapters** — intermediação nos dois sentidos. Controller não chama usecase “cru”: passa por um serviço. Usecase não fala com o banco: fala com um protocolo que um adapter (repositório, connector, handler) implementa. Cada camada tem capacidades e restrições diferentes; SOLID pesa mais no Core. Serve para CRUD HTTP e para sistemas com vários frameworks e canais. Benefícios concretos: responsabilidades claras (leitura e manutenção), flexibilidade para trocar plugin sem reescrever regra, e testes isolados por camada. ## Guia de camadas Exemplo: CRUD de usuários via REST com NestJS. Detalhes de instalação ficam de fora. Escrita **core-to-infra** (de dentro para fora). ## Core No desenho clássico, *domain* e *entities* ficam muito próximas. Aqui elas formam o **Core**: tudo o que a regra de negócio *é* — funcionalidades e representações do domínio. No exemplo, a entidade principal é Usuário (id, name), em core/entities. ### Entities // core/entities/UserEntity.ts export interface UserEntityProps { id?: string; name: string; } export class UserEntity { constructor(private readonly props: UserEntityProps) {} get id(): string { return this.props.id ?? ""; } get name(): string { return this.props.name; } } A entidade recebe props tipadas e expõe getters. Depende de uma interface que qualquer DTO de transferência pode satisfazer depois. ### Features e usecases O CRUD precisa criar, buscar, atualizar e remover. No Core, cada usecase implementa um contrato (feature) com um único método público — alinhado a Liskov, aberto/fechado, segregação de interface e responsabilidade única. O usecase **não** acessa o banco: conhece **protocols** que descrevem a ação externa (inversão de dependência). Cadastro: nome obrigatório; se já existir, erro; se não, retorna UserEntity. - contrato CreateUser - implementação CreateUserUsecase No TypeScript, classe abstrata com métodos abstratos funciona como contrato *e* valor — útil para DI (const createUserSymbol = CreateUser): // core/features/CreateUser.ts export abstract class CreateUser { abstract execute(name: string): Promise<UserEntity>; } // core/usecases/CreateUserUsecase.ts export class CreateUserUsecase implements CreateUser { constructor( private readonly createUserProtocol: CreateUserProtocol, private readonly getByNameProtocol: GetUserByNameProtocol, ) {} async execute(name: string): Promise<UserEntity> { const existsName = await this.getByNameProtocol.getByName(name); if (existsName) { throw new UserAlreadyExistsException( `the name ${name} already exists`, ); } return this.createUserProtocol.register(name); } } O usecase define *o quê* (validar nome, registrar). Não define *como* buscar ou persistir. A regra fica independente de lib, framework e banco. Cuidado: usecase que só delega ao protocol sem validar pode estar empurrando regra de negócio para o adapter. Em CreateUserUsecase, a checagem de nome duplicado é obrigação do Core. ### Exceptions UserAlreadyExistsException pertence ao Core: fluxo inválido da regra também é regra. Cada falha mapeada a uma exceção conhecida ajuda manutenção. Base com code (depois vira status HTTP na borda): // core/exceptions/IBaseException.ts export abstract class IBaseException extends Error { code: number; constructor(message: string) { super(message); } } // core/exceptions/UserAlreadyExistsException.ts export class UserAlreadyExistsException extends IBaseException { constructor(message?: string) { super(message ?? "User already exists"); this.code = 400; } } O usecase **lança** exceções; **não** as trata. Mapear tipo desconhecido → tipo conhecido fica em adapter ou infra. ### Protocols CreateUserProtocol e GetUserByNameProtocol são contratos de acesso a dispositivo externo. Protocol existe para informar ou disparar ação externa — **não** para processar regra de negócio. Preferência: um método público por protocol. // core/protocols/CreateUserProtocol.ts export abstract class CreateUserProtocol { abstract register(name: string): Promise<UserEntity>; } // core/protocols/GetUserByNameProtocol.ts export abstract class GetUserByNameProtocol { abstract getByName(name: string): Promise<UserEntity | null>; } O Core é o centro; a camada de adaptação liga o resto. ## Adapter Adapters controlam o tráfego bidirecional: externo → regra e regra → externo. Adaptam objetos, parâmetros e exceções — o mesmo espírito do [padrão Adapter](/artigos/design-patterns-adapter/). Dois grupos: - Chamados pelo Core — implementam pelo menos um protocol. - Chamados pela Infra — em geral **services**. ### Connectors, handlers e repositories Classes que implementam protocols. Cada uma adapta **um** dispositivo externo (ORM, cliente HTTP, fila, filesystem). Convenção de nomes: - **Repositories** — protocol ligado a banco (vocabulário familiar). - **Connectors** — retornam dados sem ser “tabela” (ex.: ClientHttpFetchConnector, ClientHttpAxiosConnector). - **Handlers** — processam sem retorno síncrono (ex.: publicar em Kafka). Outros nomes são válidos; o critério é um adapter por dispositivo. No CRUD, só repository (mock): // adapters/repositories/UsersMockRepository.ts export class UsersMockRepository implements GetUserByIdProtocol, GetUserByNameProtocol, CreateUserProtocol, UpdateUserProtocol, DeleteUserProtocol { private db: DbConnector; constructor() { this.db = mockDbConnector; } async getById(id: string): Promise<UserEntity> { return this.db.users.getById(id); } async getByName(name: string): Promise<UserEntity | null> { return this.db.users.getByName(name); } async register(name: string): Promise<UserEntity> { return this.db.users.register(name); } async update(id: string, name: string): Promise<UserEntity> { return this.db.users.update(id, name); } async delete(id: string): Promise<void> { return this.db.users.delete(id); } } Mock do conector: export const mockDbConnector: DbConnector = { users: { getById: async (id: string) => Promise.resolve(new UserEntity({ id, name: "Test" })), getByName: async (name: string) => Promise.resolve(new UserEntity({ id: "1", name })), register: async (name: string) => Promise.resolve(new UserEntity({ id: "2", name })), update: async (id: string, name: string) => Promise.resolve(new UserEntity({ id, name })), delete: async (_id: string) => Promise.resolve(), }, profiles: { getById: async (_id: string) => Promise.resolve(null), getByName: async (_name: string) => Promise.resolve(null), register: async (_name: string) => Promise.resolve(null), update: async (_id: string, _name: string) => Promise.resolve(null), --- # VBET: analytics sobre um SQL Server que não podíamos mudar URL: https://imrafaeldev.site/casos/analytics-sql-server-externo > Dashboard de comissões na VBET: SQL Server externo, ETL próprio e cache. A linha medida foi de cerca de sete minutos no pico até menos de um segundo com cache quente. - [Início](/) - [Casos](/casos/) - VBET: analytics sobre um SQL Server que não podíamos mudar VBET ## VBET: analytics sobre um SQL Server que não podíamos mudar O banco era de outro time. O dashboard precisava deixar de depender de um schema que não controlávamos. Engenheiro Backend Sênior out/2023 a fev/2025 ~7 min → <1 s comissões, da carga original ao cache quente Pipeline de comissões Cada estágio corresponde a uma decisão incremental documentada no case. Números de outras histórias não entram neste desenho. - [01Contexto](#contexto) - [02Restrições](#restricoes) - [03Problema](#problema) - [04Decisão](#decisao) - [05Alternativa descartada](#alternativa-descartada) - [06Resultado](#resultado) - [07Limitações](#limitacoes) ## Contexto Na VBET, entre outubro de 2023 e fevereiro de 2025, o produto de analytics servia influenciadores e afiliados de iGaming. O dashboard reunia dezenas de métricas; comissão era a leitura mais crítica. Influenciadores aceitavam pequena defasagem nos dados do dia, desde que a tela respondesse. Pagamento dependia de dados consolidados do dia anterior, não do valor ao vivo. ## Restrições O SQL Server era externo, compartilhado e não modificável de forma confiável. Índices temporários podiam ser removidos pelo proprietário da base. A API original misturava consultas SQL montadas a partir de parâmetros, com risco de injeção, e agregava demais em memória. ## Problema O sistema havia sido dimensionado para influenciadores menores. Com bases maiores, o pior pico do dashboard chegou a cerca de sete minutos. Segurança e manutenibilidade vieram antes da performance: queries cruas, pouca cobertura de testes e um caminho síncrono que recalculava demais a cada request. ## Decisão A evolução foi incremental, na ordem em que as restrições apareceram: - remover SQL inseguro, parametrizar acesso, documentar e testar; - otimizar queries e índices temporários, como mitigação e não como invariante; - paralelizar consultas independentes com Go, goroutines e channels; - quando o gargalo voltou para o SQL Server, criar ETL e PostgreSQL próprios, com pré-cálculo, checkpoints e reconciliação; - separar leitura REALTIME (tendência, consistência eventual) de CLOSED (precisão financeira e pagamento); - cache-aside com TTL alinhado à defasagem aceita de cerca de cinco minutos; - degradação controlada se o cache falhasse, em vez de derrubar a tela. ## Alternativa descartada Insistir em índices na base externa como arquitetura, ou recalcular anos de histórico a cada acesso. Também descartada a ideia de pagar o afiliado com o dado REALTIME. ## Resultado A linha reconciliada no dossiê da experiência, para o caminho de comissão/dashboard, é: - pior pico inicial: cerca de 7 minutos; - após queries e índices: cerca de 3 minutos; - após paralelização: cerca de 1 minuto; - após ETL/PostgreSQL: cerca de 15 segundos no p99 da comissão sem cache; - cache quente: menos de 1 segundo. Cada número pertence a essa etapa. Não descreve o ganho de uma decomposição posterior em microsserviços. ## Limitações A investigação, a decisão de ETL + base própria, a separação REALTIME/CLOSED e a política de degradação são o núcleo atribuível aqui. A decomposição do monólito em Kubernetes é outra história e não mistura o “500%” nem a latência abaixo de 60 ms com este caso. Versões antigas de currículo que citam 30 segundos em carga fria, 11 segundos ou percentuais de SLA sem cenário ficam de fora. Se o produto passar a exigir precisão realtime no pagamento, a separação CLOSED deixa de ser suficiente. Contato ## Tem um sistema que deixou de ser simples? Chame direto pelo @imrafaeldev, sem formulário — para conversa profissional, comece pelo LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Abrir a página de contato](/contato/) --- # Estudos de caso URL: https://imrafaeldev.site/casos > Cada case documenta a restrição, a decisão, a alternativa descartada e o resultado medido. Também registra quando a decisão deve ser revista. - [Início](/) - Casos ## Estudos de caso Cada case documenta a restrição, a decisão, a alternativa descartada e o resultado medido. Também registra quando a decisão deve ser revista. - Infosistemas fev/2025 a mai/2026 ## [Infosistemas: contrato de falha na mensageria RabbitMQ](/casos/mensageria-rabbitmq/) Falhas intermitentes entre microsserviços sem contrato para retry, DLQ ou duplicidade. Mais consumidores só empurravam a sobrecarga. **~98%** redução de falhas intermitentes nos fluxos críticos [Abrir o case — Infosistemas: contrato de falha na mensageria RabbitMQ](/casos/mensageria-rabbitmq/) - VBET out/2023 a fev/2025 ## [VBET: analytics sobre um SQL Server que não podíamos mudar](/casos/analytics-sql-server-externo/) O banco era de outro time. O dashboard precisava deixar de depender de um schema que não controlávamos. **~7 min → <1 s** comissões, da carga original ao cache quente [Abrir o case — VBET: analytics sobre um SQL Server que não podíamos mudar](/casos/analytics-sql-server-externo/) - Flapper set/2021 a jun/2022 ## [Flapper: descobrir o domínio antes de separar o monólito](/casos/modernizacao-monolito-sem-documentacao/) O produto não podia parar, e os autores originais já não estavam lá. Antes de migrar, foi preciso descobrir quais fronteiras o banco ainda revelava. **~25%** menos tabelas na separação por domínios [Abrir o case — Flapper: descobrir o domínio antes de separar o monólito](/casos/modernizacao-monolito-sem-documentacao/) --- # Infosistemas: contrato de falha na mensageria RabbitMQ URL: https://imrafaeldev.site/casos/mensageria-rabbitmq > Redesenho da mensageria RabbitMQ na Infosistemas com filas duráveis, DLQ, retry, idempotência e prefetch. O trabalho reduziu em cerca de 98% as falhas intermitentes entre microsserviços. - [Início](/) - [Casos](/casos/) - Infosistemas: contrato de falha na mensageria RabbitMQ Infosistemas ## Infosistemas: contrato de falha na mensageria RabbitMQ Falhas intermitentes entre microsserviços sem contrato para retry, DLQ ou duplicidade. Mais consumidores só empurravam a sobrecarga. Engenheiro de Software Sênior / Arquiteto de Software fev/2025 a mai/2026 ~98% redução de falhas intermitentes nos fluxos críticos Contrato de falha na mensageria Publicação confirmada, consumo com prefetch controlado, retry com backoff e DLQ por fluxo. Escalar só consumidores fica de fora do desenho. - [01Contexto](#contexto) - [02Restrições](#restricoes) - [03Problema](#problema) - [04Decisão](#decisao) - [05Alternativa descartada](#alternativa-descartada) - [06Implementação](#implementacao) - [07Resultado](#resultado) - [08Limitações](#limitacoes) ## Contexto A Infosistemas opera plataformas de gestão para locadoras, frotas e montadoras. O trabalho ocorreu no time de arquitetura, em colaboração com DevOps, SREs e DBAs, entre fevereiro de 2025 e maio de 2026. Este caso cobre a frente de mensageria. Outras frentes da mesma experiência (segurança de ERP, jornadas de assinatura, integrações fiscais) existem nas fontes, mas não entram aqui como número ou afirmação extra. ## Restrições Os fluxos críticos cruzavam microsserviços. A falha era intermitente: a mesma operação podia completar numa execução e não completar na seguinte. Aumentar concorrência ou prefetch sem critério transferia sobrecarga para consumidores, serviços ou bancos downstream. ## Problema Mensagens deixavam de completar o fluxo esperado. Investigar falha parcial era difícil. Não havia contrato explícito para falha temporária, falha permanente, duplicidade ou poison message. ## Decisão A mensageria foi redesenhada para tornar o comportamento em falha previsível: - filas duráveis; - DLQ por fluxo, para mensagem que não deve desaparecer nem repetir sem controle; - retry com backoff para indisponibilidade temporária; - idempotência e deduplicação no consumidor, porque entrega duplicada não pode repetir efeito de negócio; - publisher confirms, para reduzir incerteza na publicação; - ajuste de prefetch, em vez de abrir concorrência indiscriminada. ## Alternativa descartada Tratar o problema como falta de capacidade (mais consumidores, mais prefetch) sem mudar o contrato de falha. Isso moveria o gargalo e manteria perda ou duplicidade silenciosa. ## Implementação O redesenho colocou esses mecanismos nos fluxos críticos: publicação confirmada, fila durável com prefetch controlado, consumidor idempotente, retry com backoff e DLQ por fluxo. A operação passou a ter um caminho previsível para falha temporária e para falha permanente, em vez de depender de reprocessamento ad hoc. ## Resultado A redução registrada nas falhas intermitentes dos fluxos críticos entre microsserviços foi de aproximadamente 98%. O número descreve esses fluxos após o redesenho, não a operação inteira da empresa nem outras frentes. ## Limitações Métricas de outras frentes ainda pendentes de método ou confirmação ficam de fora. Se a volumetria ou o mapa de microsserviços mudar de forma que DLQ e prefetch deixem de isolar a falha, o tuning precisa ser revisto com telemetria de fila e de consumidores. Contato ## Tem um sistema que deixou de ser simples? Chame direto pelo @imrafaeldev, sem formulário — para conversa profissional, comece pelo LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Abrir a página de contato](/contato/) --- # Flapper: descobrir o domínio antes de separar o monólito URL: https://imrafaeldev.site/casos/modernizacao-monolito-sem-documentacao > Modernização incremental de um monólito PHP sem documentação na Flapper: banco como fonte de descoberta, bounded contexts e Strangler. A separação por domínios reduziu em aproximadamente 25% o número de tabelas. - [Início](/) - [Casos](/casos/) - Flapper: descobrir o domínio antes de separar o monólito Flapper ## Flapper: descobrir o domínio antes de separar o monólito O produto não podia parar, e os autores originais já não estavam lá. Antes de migrar, foi preciso descobrir quais fronteiras o banco ainda revelava. Engenheiro de Software Full Stack set/2021 a jun/2022 ~25% menos tabelas na separação por domínios Descoberta de domínio antes da migração O banco legado revela fronteiras; pessoas, autenticação e aeronaves saem gradualmente para contextos com persistência própria. - [01Contexto](#contexto) - [02Restrições](#restricoes) - [03Problema](#problema) - [04Decisão](#decisao) - [05Alternativa descartada](#alternativa-descartada) - [06Resultado](#resultado) - [07Limitações](#limitacoes) ## Contexto Na Flapper, o produto principal era um monólito PHP com mais de sete anos, pouca documentação útil e sem os desenvolvedores que o haviam criado. A aplicação sustentava a operação de aviação executiva e não podia ser interrompida para uma reescrita. ## Restrições O código e o banco acumulavam regras e dependências difíceis de explicar. Alterações tinham efeitos colaterais pouco previsíveis, e não havia especialistas remanescentes para confirmar como cada parte do sistema deveria evoluir. A migração precisava coexistir com o produto em produção. ## Problema Trocar PHP por outra tecnologia não responderia à dúvida principal: quais regras pertenciam juntas e quais dependências poderiam ser separadas sem quebrar a operação. O sistema precisava de fronteiras de domínio antes de serviços novos. ## Decisão Usei o banco e o código existente como fonte de descoberta. Agrupamentos de tabelas e relações ajudaram a identificar bounded contexts; a partir deles, a migração seguiu o padrão Strangler: - extrair um domínio por vez, sem interromper o monólito; - manter em cada contexto apenas a representação local dos dados de que precisava; - propagar mudanças por eventos em Kafka, em vez de conectar todos os serviços ao banco antigo; - usar Node.js, NestJS e Go nos primeiros módulos, com gRPC, REST ou GraphQL conforme o consumidor; - documentar a estratégia e os primeiros módulos para que o time pudesse continuar a transformação. ## Alternativa descartada Reescrever o monólito inteiro, ou manter serviços novos presos ao mesmo banco e às mesmas relações compartilhadas. A primeira opção pararia o negócio; a segunda preservaria o acoplamento que a migração precisava reduzir. ## Resultado A separação por domínios reduziu em aproximadamente 25% o número de tabelas e criou sete bancos organizados por contexto. Os módulos de pessoas, autenticação e aeronaves foram os primeiros passos de uma transformação planejada para continuar além da entrega inicial. ## Limitações O número mede a redução de tabelas nessa separação de domínios, não um ganho financeiro, a migração completa ou um resultado de mercado da empresa. A data final da experiência tem divergência histórica em fontes antigas; o período publicado segue o perfil exportado. Se um domínio ainda depender de regras não mapeadas no monólito, sua extração precisa ser adiada ou receber uma integração de transição explícita. Contato ## Tem um sistema que deixou de ser simples? Chame direto pelo @imrafaeldev, sem formulário — para conversa profissional, comece pelo LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Abrir a página de contato](/contato/) --- # Contato URL: https://imrafaeldev.site/contato > Fale com Rafael Pereira pelo @imrafaeldev no LinkedIn, GitHub, Instagram e YouTube. Sem formulário: escolha o canal e chame direto. - [Início](/) - Contato Contato ## Tem um sistema que deixou de ser simples? Chame direto pelo @imrafaeldev, sem formulário. Para conversa profissional, comece pelo LinkedIn; para ver decisões em código, vá ao GitHub. - [Instagram @imrafaeldev](https://www.instagram.com/imrafaeldev/) - [YouTube @imrafaeldev](https://www.youtube.com/@imrafaeldev) - [GitHub @imrafaeldev](https://github.com/imrafaeldev) - [LinkedIn @imrafaeldev](https://www.linkedin.com/in/imrafaeldev/) --- # Currículo URL: https://imrafaeldev.site/curriculo > Currículo online de Rafael Pereira, engenheiro backend sênior com experiência em Node.js, Go e Java, e atuação complementar em React e Angular. - [Início](/) - Currículo ## Engenharia para quando sistemas deixam de ser simples Engenheiro Backend Sênior Engenheiro Backend Sênior com mais de sete anos de experiência em sistemas distribuídos, plataformas transacionais e modernização de legados. Atuo com execução hands-on, arquitetura, liderança técnica, mentoria e colaboração com produto e stakeholders. Meu eixo principal é Node.js, Go e Java; React e Angular entram como atuação complementar em produtos que exigem continuidade entre backend e frontend. ## Experiência - fev/2025 a mai/2026 Remoto Infosistemas Engenheiro de Software Sênior / Arquiteto de Software Expandir experiência Recolher experiência Liderei integrações em NestJS e Go e redesenhei fluxos entre microsserviços, reduzindo em cerca de 98% as falhas intermitentes nos fluxos críticos. Contexto Atuei em plataformas de gestão para locadoras, frotas e montadoras, no time de arquitetura e em colaboração com especialistas de operação e dados. Contribuição Liderei integrações em NestJS e Go e redesenhei fluxos entre microsserviços, tornando o comportamento diante de falhas mais previsível nos fluxos críticos. [Ler experiência completa](/experiencias/infosistemas/) - jul/2025 a dez/2025 Remoto EDS (Polícia Civil do Rio de Janeiro) Engenheiro Backend — Consultoria Expandir experiência Recolher experiência Estruturei um sistema de gestão de saúde com NestJS para uma operação pública crítica e evoluí rotas backend com foco em segurança, acesso e rastreabilidade. Contexto A consultoria envolveu sistemas públicos sensíveis, com um sistema de gestão de saúde de alto volume e um ERP jurídico em evolução. Contribuição Estruturei backend com NestJS, refatorei rotas legadas e reforcei segurança, controle de acesso e rastreabilidade de fluxos sensíveis. [Ler experiência completa](/experiencias/eds-policia-civil-rio/) - mar/2025 a jun/2025 Remoto Azify Engenheiro Backend Sênior — Consultoria Expandir experiência Recolher experiência Desenvolvi um motor de liquidação em NestJS e reduzi em 30% a latência de APIs financeiras críticas por meio de profiling e otimização. Contexto Atuei em fintech e criptoativos, em serviços financeiros nos quais consistência, segurança e estabilidade tinham impacto direto na operação. Contribuição Desenvolvi um motor de liquidação em NestJS com integrações financeiras e reduzi em 30% a latência de APIs críticas por meio de profiling e otimização. [Ler experiência completa](/experiencias/azify/) - out/2023 a fev/2025 Remoto VBET Engenheiro Backend Sênior Expandir experiência Recolher experiência Apliquei Go, goroutines e channels para reduzir a primeira etapa do cálculo de comissões de cerca de sete para três minutos, além de atuar na evolução da aplicação React. Contexto O produto de analytics atendia influenciadores e afiliados de iGaming; o dashboard reunia métricas de comissão e aceitava pequena defasagem para dados do dia, enquanto pagamentos exigiam dados consolidados. Contribuição Reestruturei a API e o caminho de cálculo com Go, goroutines e channels, introduzi ETL, pré-cálculo e cache e colaborei na evolução da aplicação React. A linha medida foi de cerca de sete minutos no pico para cerca de três minutos, cerca de um minuto, aproximadamente 15 segundos sem cache e menos de um segundo com cache quente, conforme cada etapa. [Ler experiência completa](/experiencias/vbet/) - abr/2023 a out/2023 Remoto Maxmilhas Engenheiro de Software Full Stack Expandir experiência Recolher experiência Desenvolvi microsserviços em Node.js e NestJS para automatizar cancelamentos e remarcações, reduzindo em 34% a intervenção manual do suporte. Contexto Atuei em fluxos de pós-venda de viagens, incluindo cancelamentos, remarcações, cupons e comunicação com clientes. Contribuição Desenvolvi microsserviços em Node.js e NestJS e automatizei regras, cálculos e validações de pós-venda; a mudança reduziu em 34% a necessidade de intervenção manual do suporte. [Ler experiência completa](/experiencias/maxmilhas/) - jun/2022 a abr/2023 Remoto South System (alocado na QUIQ/Itaú) Engenheiro Backend Expandir experiência Recolher experiência Arquitetei um marketplace multi-tenant com Node.js e serviços assíncronos em Go, estruturando testes críticos com aproximadamente 95% de cobertura. Contexto O trabalho era voltado a um marketplace white-label e multi-tenant para instituições financeiras, preparado para incorporar novos bancos sem forks por cliente. Contribuição Participei da arquitetura e do refinamento de regras, usando Node.js e serviços assíncronos em Go para isolar variações por tenant; estruturei testes críticos com aproximadamente 95% de cobertura. [Ler experiência completa](/experiencias/south-system-quiq-itau/) - set/2021 a jun/2022 Remoto Flapper Engenheiro de Software Full Stack Expandir experiência Recolher experiência Migrei módulos de pessoas, autenticação e aeronaves para serviços em Node.js, NestJS e Go; a separação por domínios reduziu em aproximadamente 25% o número de tabelas. Contexto A plataforma de aviação executiva dependia de um monólito com mais de sete anos, pouca documentação e regras difíceis de separar sem interromper a operação. Contribuição Mapeei fronteiras de domínio e conduzi uma modernização incremental, migrando módulos para serviços em Node.js, NestJS e Go; a separação reduziu em aproximadamente 25% o número de tabelas. [Ler experiência completa](/experiencias/flapper/) - jan/2021 a ago/2021 Remoto Sustentec Engenheiro de Software Full Stack Pleno Expandir experiência Recolher experiência Mantive e evoluí um sistema de gestão de laboratórios em Java, Spring Boot e Angular, implementando testes de integração antes inexistentes. Contexto Atuei em sistemas ligados a laboratórios, pesquisa e desenvolvimento, combinando manutenção de produto existente com evolução funcional. Contribuição Mantive e evoluí o sistema com Java, Spring Boot e Angular, implementei testes de integração e entreguei relatórios e funcionalidades ponta a ponta. [Ler experiência completa](/experiencias/sustentec/) - nov/2019 a jan/2021 Campina Grande, Paraíba / Remoto Braistech Engenheiro de Software Full Stack Pleno Expandir experiência Recolher experiência Liderei a estruturação do sistema principal com Node.js e NestJS e projetei microsserviços para o núcleo do negócio. Contexto Em um produto com domínio de contratos de criptoativos e movimentações financeiras, assumi responsabilidade ampla em uma equipe pequena. Contribuição Liderei a estruturação do sistema principal com Node.js e NestJS, projetei microsserviços para o núcleo do negócio e ajudei a evoluir o código para Clean Architecture, além de orientar desenvolvedores juniores. [Ler experiência completa](/experiencias/braistech/) ## Especialidades - ### Node.js Especialidade em APIs, microsserviços e integrações com NestJS. - ### Go Experiência avançada em APIs, concorrência e processamento de alto volume. - ### Java Experiência em manutenção e evolução de sistemas com Spring Boot. - ### React Atuação na evolução e manutenção de aplicações React. - ### Angular Atuação em aplicação web Angular com formulários, validações e consumo de API. ## Formação - ### Análise e Desenvolvimento de Sistemas Tecnólogo · UNOPAR — Universidade Norte do Paraná concluído em 2024 - ### Computação em Nuvem Pós-graduação · Anhanguera Educacional concluída em 2025 ## Links públicos - [LinkedIn — Abrir link público](https://www.linkedin.com/in/this-rafael-pereira/) - [GitHub — Abrir link público](https://github.com/this-rafael) --- # Most backend performance problems start close to the data URL: https://imrafaeldev.site/en/articles/backend-performance-close-to-data > Before adding machines, cache, or queues, measure the request - [Home](/en/) - [Articles](/en/articles/) - Most backend performance problems start close to the data ## Most backend performance problems start close to the data Slow API and the meeting already fills with solutions. I start close to the data — not to blame the database, but because that check usually gives a fast signal. July 16, 2026 - [Performance](/en/articles/?topic=performance) - [Architecture](/en/articles/?topic=arquitetura) A slow API and the meeting already fills with solutions before anyone has a measurement. More CPU and RAM show up, cache, queues, microservices, refactoring. Sometimes someone even proposes switching languages. Rarely is the first suggestion to open the execution plan. That is why I start the investigation close to the data. Not because the database is the default culprit, but because so much passes through it and that check usually gives a fast signal. If the query is healthy, I rule the database out and follow the request flow. ## Quick fixes can also hide expensive work Cache and queues solve real problems. Bigger machines can also be the right call. When they arrive by reflex, though, those resources may only change the bill size while nobody knows which part of the request is holding the response. More CPU reduces resource contention, cache takes some requests out of the path, and a queue absorbs a spike. Meanwhile, a query doing enormous work to return almost nothing stays expensive on every execution. The same goes for a loop firing one call per item. Latency may drop for a while, the alert stops firing, and the team breathes. When load grows, the expensive operation reappears, now accompanied by larger infrastructure. The question that must come before the architecture debate: how much work is this request producing to deliver the result? ## We almost doubled the machine and the dashboard stayed slow That is what happened with a dashboard I worked on. It loaded synchronously, and a single query did everything at once: fetched data, joined several pieces of information, and computed the displayed values. Since database CPU ran very high, we almost doubled CPU and RAM. The database got bigger. The dashboard still took over a minute to load. The same query still concentrated all heavy work in a single execution. That was when we opened the execution plan. The screen returned few indicators, but the query crossed relationships, formed a large intermediate volume, and spent CPU on aggregations before reaching them. The final answer was small. The work to produce it, enormous. Doubling CPU and RAM had given the database more headroom, but the search still forced it to do everything at once. The plan showed the investigation had to enter the path traveled by the query. ## What I need to see before touching the database For the database to become a real suspect, I want the execution plan and metrics pointing that way. Query time, reads, and cardinality usually confirm or eliminate a hypothesis in minutes. If those measures are healthy, I rule the database out. I have caught slow APIs with the query responding within expectations, while the delay sat in application processing and sequential remote calls. From there, continuing to hunt a database defect would only insist on the wrong layer. Before going deeper, I run a short radar over the query and the code. One query to load the list followed by another per record calls for a query count. That N+1 shows up often when the ORM leaves relationships for the backend to fetch one by one. A JOIN multiplying rows before aggregation calls for measuring intermediate volume. A missing index calls for the plan. A filter applying a function over the indexed column too. In code, I look for remote calls or queries with await inside a for. Each wait may look small alone and still dominate total time when all run in a queue. None of these signals closes the diagnosis. An N+1 on a route with two items may have irrelevant impact, a new index may help little on a low-selectivity column, and a voluminous join may be needed to produce the result. So I count queries, time the whole loop, and check in the plan how many rows and reads were produced. That radar only picks the first measurement. The next investigation layer comes from the math. ## One correct line can waste the index One of those lines often passes review unnoticed. It returns a full day’s records. WHERE CAST(datetime_column AS date) = @date The screen result looks correct. In the plan, the story may differ. Applying CAST to the column forces the query to transform values before comparison. With an index on datetime_column, that can prevent a direct range seek, raise reads substantially, and even lead to a scan. When the request is for a full day’s records, I compute the boundaries outside the column. WHERE datetime_column >= @start_of_day AND datetime_column < @start_of_next_day If the start is July 16 at midnight, the next bound is July 17 at midnight. That includes every value on the 16th, including those with fractional seconds at the end, without depending on 23:59:59.999. The result stays correct, but now there is a range the index can walk. Confirmation comes from comparing reads and the access operator in both plans. If the metric does not change, the hypothesis did not hold. ## Read the plan by the work sequence After comparing filter versions, I read the plan as a story of the work produced by the query. I start with total time and reads. If the screen returns ten indicators but execution performs hundreds of thousands of reads, there is a bill to explain. Then I compare estimated vs actual cardinality. When the optimizer expected few rows and received many, it may have picked joins, memory, and aggregations for a far smaller scenario than it met at runtime. Next I follow where data grows. I look for the operator where a JOIN takes thousands of rows to hundreds of thousands, just before a filter or GROUP BY shrinks everything again. The small final answer may hide an enormous intermediate volume. That is where aggregations and sorts start spending too much CPU. I also do not take the plan’s displayed cost percentage as verdict. It serves to choose where to measure. I mark the expensive operator, how many rows enter and leave it, and how much time it consumes. If cost shows up after row multiplication and before the few needed indicators, there is already a concrete hypothesis to test. ## Split the query where cost grows What fixed the dashboard was splitting the query and using indexes properly. The split did not happen arbitrarily. The plan itself showed where cost started growing. We kept in the database the filters and indexed lookup of the needed records. Moving that slice to the application would make the server receive a larger set before it could discard it. It would also waste the path indexes already shortened. Final indicator composition moved to the application. That stage came after the relationships and aggregations that raised intermediate volume and kept database CPU high. With data already sliced, the server could assemble screen values without concentrating all execution in a single query. The database started reducing the set early. The application received the selected records and composed the indicators. After the change, the dashboard stopped depending on that heavy query and became more predictable, because the split followed the point where cost grew in the plan. That boundary did not come from a preference for single queries or for more application logic. I marked where CPU, reads, and intermediate rows spiked after selective filters. Then I checked whether the expensive stretch could receive an already-reduced set outside the database. Before moving that stage, two bills had to balance. The database should keep doing the slicing that used the indexes. The application should compose the result without fetching too many rows, creating per-item --- # Derusting logic #02: Container With Most Water URL: https://imrafaeldev.site/en/articles/derusting-logic-container-with-most-water > LeetCode 11 in JavaScript: a neighbor-based attempt, the hypothesis review, and the two-pointer solution. - [Home](/en/) - [Articles](/en/articles/) - Derusting logic #02: Container With Most Water ## Derusting logic #02: Container With Most Water I tried to pick the next step by looking only at the neighbors. It worked in some cases, but the problem asked for a wider view. September 15, 2026 - [Performance](/en/articles/?topic=performance) - [Trade-offs](/en/articles/?topic=trade-offs) After solving the first exercise of the series, I stayed on LeetCode to work on logic without asking for a ready-made solution. Challenge 011 is [Container With Most Water](https://leetcode.com/problems/container-with-most-water/). We get an array of heights and need to pick two lines that form the container with the largest area. ## How to compute the area If I pick positions left and right, the width is the distance between them. The container height is limited by the shorter of the two lines. area = min(left height, right height) × distance For example, with these heights: [1, 8, 6, 2, 5, 4, 8, 3, 7] The lines at positions 1 and 8 have heights 8 and 7. The shorter height is 7, and the distance between them is 7. That combination produces an area of 49. ## My first attempt I started with two pointers, one at each end of the array. After computing the current area, I simulated two possibilities: - advancing the left pointer; - moving the right pointer back. I computed the area of the next two pairs and picked the larger one. The main excerpt was this: const paddingLeftArea = Math.min(heights[leftIndex + 1], heights[rigthIndex]) * (rigthIndex - leftIndex + 1); const paddingRightArea = Math.min(heights[leftIndex], heights[rigthIndex - 1]) * (rigthIndex - 1 - leftIndex); if (paddingLeftArea > paddingRightArea && paddingLeftArea > maxArea) { leftIndex += 1; } else { rigthIndex -= 1; } The problem was in the hypothesis. The best local decision does not guarantee the best area over the rest of the array. I tried to guess the path by looking only at the next two moves. There was also an error in the distance formula of that draft: for a pair of positions, the width is right - left. ## The observation that unlocks the problem The area depends on two things: width and shorter height. When the pointers are at positions left and right, moving the pointer of the taller line cannot raise the container’s minimum height. The width always shrinks, and the height limiting the area is still there. So the pointer that must advance is the one at the shorter height. It is the only move that can find a taller line and make up for the lost width. If the heights are equal, either one can advance. In the code, I chose to advance the left one when heights[leftIndex] <= heights[rigthIndex]. ## Two-pointer solution /** * @param {number[]} heights * @return {number} */ var maxArea = function (heights) { let leftIndex = 0; let rigthIndex = heights.length - 1; let maxArea = 0; while (leftIndex < rigthIndex) { const minH = Math.min(heights[leftIndex], heights[rigthIndex]); const currentArea = minH * (rigthIndex - leftIndex); maxArea = Math.max(maxArea, currentArea); if (heights[leftIndex] <= heights[rigthIndex]) { leftIndex++; } else { rigthIndex--; } } return maxArea; }; Each round, I compute the current pair’s area, update the largest area found, and move one of the pointers. The while loop converges toward the center and ends. The advancing detail matters. In the version I had written, the pointers only advanced when the current area was not larger than maxArea. If a new maximum area was found, the same combination would be computed again, never leaving the loop. The fix was to separate the two decisions: record the area, then move the shorter-height pointer. ## The result of the attempts The LeetCode history looked like this: - JavaScript: accepted, 3 ms and 63.6 MB. - JavaScript: wrong answer. - JavaScript: wrong answer. - Go: accepted, 0 ms and 9.6 MB. - TypeScript: accepted, 3 ms and 63.9 MB. - Go: wrong answer. There were three wrong-answer attempts before reaching the accepted solutions in JavaScript, Go, and TypeScript. More than counting submissions, I wanted to look at the error, understand the hypothesis that failed, and try again without outsourcing all the reasoning. ## Complexity The algorithm walks the array once. On each iteration, one of the pointers advances, so time complexity is O(n) and space complexity is O(1). The first attempt also used two pointers but did extra work comparing future possibilities. The second solution uses a property of the problem to safely discard part of the combinations. That was the exercise this time: not mistaking a choice that looks good now for a decision the problem actually lets you justify. --- *Derusting logic series #02 — Container With Most Water. Problem at [leetcode.com/problems/container-with-most-water](https://leetcode.com/problems/container-with-most-water/).* Geometria: min da altura × largura Nas posições 1 e 8, a menor altura é 7 e a largura é 7. O recipiente produz área 49. Dois ponteiros: mova a menor linha A largura sempre diminui. Só mover a menor altura pode encontrar um limite maior; mover a maior mantém o gargalo e pode ser descartado. --- # Derusting logic #01: Group Anagrams URL: https://imrafaeldev.site/en/articles/derusting-logic-group-anagrams > LeetCode 49 in Go: from sort-based keys to 26-letter counting, and the habit of keeping thinking after the code works. - [Home](/en/) - [Articles](/en/articles/) - Derusting logic #01: Group Anagrams ## Derusting logic #01: Group Anagrams I had not stopped writing code. What changed was outsourcing parts of the reasoning. Group Anagrams was the exercise to recover the habit. August 17, 2026 - [Trade-offs](/en/articles/?topic=trade-offs) - [Performance](/en/articles/?topic=performance) After years programming, I realized my logic was rusty. I had not stopped writing code. What changed was that, little by little, I started outsourcing parts of the reasoning I used to exercise alone. I picked exercise [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/) on LeetCode. The task is to receive a list of strings and group anagrams together. ## What we need to solve Input: ["eat", "tea", "tan", "ate", "nat", "bat"] Possible result: ["eat", "tea", "ate"] ["tan", "nat"] ["bat"] Group order does not matter. ## What is an anagram? Take eat, tea, and ate. Each has a once, e once, and t once. Positions change; the count of each letter stays the same. The algorithm must turn those words into a shared representation. If all three produce the same key, I can use that key in a map and place them in the same group. The first problem is creating that key. ## My first answer was sorting I started using the sorted string as the key: eat → aet tea → aet ate → aet All three produce aet. ### Sort-based solution That was the first solution that came to mind. I was not trying for the leanest implementation right away. I wanted a coherent solution and to understand where it could improve. func sortString(str string) string { b := []byte(str) slices.Sort(b) return string(b) } func groupAnagrams(strs []string) [][]string { mapping := make(map[string][]string) for _, str := range strs { sortedStr := sortString(str) mapping[sortedStr] = append(mapping[sortedStr], str) } result := make([][]string, 0, len(mapping)) for _, group := range mapping { result = append(result, group) } return result } What happens in this code: - sortString(str) turns the string into bytes, sorts the characters, and returns a new string. - sortedStr := sortString(str) produces the key for that term. - mapping[sortedStr] = append(...) uses that key to accumulate anagrams in the same group. - For eat, tea, and ate, sortedStr is always aet. The map starts looking like this: "aet" -> ["eat", "tea", "ate"] "ant" -> ["tan", "nat"] "abt" -> ["bat"] I compute one key and add the word directly to the matching group. The map avoids comparing every string with every other. ## Where is the cost of this approach? To discover the key, I sort each string. If a string has k characters, that sort costs roughly O(k log k). Repeating for n strings, the dominant part is O(n × k log k). ### Why drop the sort? For eat, I sorted characters to reach aet. For tea, I sorted again to reach the same aet. Sorting works because it creates a shared representation. But the problem does not require sorting anything. To know whether two strings are anagrams, it is enough to check whether they hold the same count of each letter. ## That observation changes the solution Instead of placing letters in the same order, I can count how many times each appears. In this exercise, inputs use lowercase letters from a to z. Each string can be represented by 26 counters. tea and ate produce the same count. I do not need to rearrange any character. I just walk the string and count occurrences. For eat, the relevant part is: a = 1 e = 1 t = 1 ### Counting solution var key [26]uint8 creates 26 slots, one per letter from a to z. key[str[i]-'a']++ finds each character’s slot and increments the counter. Then groups[key] = append(groups[key], str) uses the frequency vector itself as the group key. func groupAnagrams(strs []string) [][]string { groups := make(map[[26]uint8][]string, len(strs)) for _, str := range strs { var key [26]uint8 for i := 0; i < len(str); i++ { key[str[i]-'a']++ } groups[key] = append(groups[key], str) } result := make([][]string, 0, len(groups)) for _, group := range groups { result = append(result, group) } return result } ## What changed in complexity? - Sorting: O(k log k) per string and O(n × k log k) for n strings. - Counting: O(k) per string and O(n × k) for n strings. The improvement appeared when I realized sorting did work the problem never asked for. The main change is from O(k log k) to O(k) per string. ## The first solution was not wrong It solves the problem and, depending on context, could be enough. The habit I wanted to recover was to keep thinking after the code starts working. Find a solution, return to the problem, and ask: what work is my algorithm doing without needing to? I am not doing these exercises because LeetCode represents all software engineering work, nor to chase the most sophisticated solution. I am doing them because I noticed that using AI every day reduced how often I insist on a problem alone. I want to reserve space to exercise that again: read, try, fail, review the approach, and only then compare paths. --- *Series Derusting logic #01 — Group Anagrams. Adapted from the original carousel; problem at [leetcode.com/problems/group-anagrams](https://leetcode.com/problems/group-anagrams/).* Chave por sort e mapa Cada string vira chave ordenada (eat → aet). O mapa agrupa anagramas sob a mesma chave sem comparar cada par. Contagem [26]uint8 vs sort Vetor de frequências a–z vira chave. Contagem custa O(k) por string; sort custava O(k log k). Total: de O(n × k log k) para O(n × k). --- # Stop being hostage to dependencies. Say hello to the Adapter design pattern URL: https://imrafaeldev.site/en/articles/design-patterns-adapter > With the Adapter, the service depends on a protocol and adapters translate MySQL, PostgreSQL, or mocks — without coupling business rules to the driver. - [Home](/en/) - [Articles](/en/articles/) - Stop being hostage to dependencies. Say hello to the Adapter design pattern ## Stop being hostage to dependencies. Say hello to the Adapter design pattern Migrating from MySQL to PostgreSQL does not require rewriting the service. The Adapter isolates the plugin behind a contract the business rule understands. April 26, 2022 - [Architecture](/en/articles/?topic=arquitetura) With the Adapter pattern, we isolate the business rule from the concrete dependency. ## Starting scenario We have a backend with a simple user CRUD: create, edit, retrieve, and delete through API endpoints. Data goes to some database — say MySQL — and the structure starts as **Controller → Service → Database**. So far, the team is comfortable. Until someone decides: next week we migrate from MySQL to PostgreSQL. From there, the house falls down on technology. Beyond restructuring the database, the team must hunt down MySQL references: inserts, connection, scattered queries. Most of the time this delays delivery, reduces quality, skips tests, and introduces bugs. It would be better to reduce the dependency between the business rule and whoever performs the specific operation — here, the database. The Adapter helps build the system that way. ## Adapter pattern We have a third-party plugin, library, module, or service that does something we want in the business rule — here, persisting data. The path: - Define an interface with the contract of what we need. - Expose only methods that make sense in context (SOLID). - Implement the interface in classes that adapt the third-party code. We create CreateDatabaseCustomerProtocol with a create method that receives CustomerInputEntity and returns SuccessfulEntityCreation: interface CustomerInputEntity { name: string; email: string; birthDate: Date; } interface SuccessfulEntityCreation { readonly id: number; readonly name: string; readonly email: string; readonly birthDate: Date; } interface CreateDatabaseCustomerProtocol { createCustomerOnDatabase( customer: CustomerInputEntity, ): SuccessfulEntityCreation; } Instead of **Controller → Service → Database**, we move to **Controller → Service → Protocols → Plugin**. The service loses knowledge of how the CRUD reaches the database. It is composed of protocols; the concrete implementation arrives at runtime — via dependency injection. While we use PostgreSQL, we implement the protocols in adapters (or connectors). CreateDatabaseCustomerProtocol can be implemented by CreateDatabaseCustomerPostgresqlAdapter, CreateDatabaseCustomerMysqlAdapter, CreateDatabaseCustomerMongoDBAdapter, or CreateDatabaseCustomerMockedAdapter. The service looks like this: class CustomerService { constructor( private readonly createCustomer: CreateDatabaseCustomerProtocol, ) {} public register(customer: CustomerInputEntity): SuccessfulEntityCreation { return this.createCustomer.createCustomerOnDatabase(customer); } } For the service, it does not matter whether the database returns JSON, XML, or another format — the adapter translates to the contract the business rule expects. ## Advantages - **Maintenance:** any plugin can be replaced without rewriting the service. - **Tests:** to test only the business rule, inject a mock adapter implementing the same protocol. - **Clean code:** separated responsibilities; the business rule does not carry driver details. ## Next step Take a frontend with dozens of libraries and identify what you actually use. Pick one feature — converting BRL to USD, for example. Describe the contract (input and output) and implement an adapter on top of the library that does it today. Repeat where the dependency hurts. ## Relation to other patterns In the article [Design Patterns: Strategy](/en/articles/design-patterns-strategy/), the focus is swapping algorithms behind a contract. The Adapter isolates external dependencies behind an owned interface. The two complement each other: Strategy varies behavior; Adapter translates the outside world. --- *Originally published on [LinkedIn](https://www.linkedin.com/pulse/pare-de-ser-ref%C3%A9m-das-depend%C3%AAncias-diga-bem-vindo-ao-design-rafael/) on April 26, 2022.* Adapter: protocolo e implementações CustomerService usa CreateDatabaseCustomerProtocol. MysqlAdapter e PostgresqlAdapter implementam o contrato e traduzem para cada banco. Camadas: antes e depois Acoplamento direto ao MySQL dificulta migração. Com protocolo e adapter, troca-se o plugin sem reescrever o serviço. --- # Design Patterns: Strategy URL: https://imrafaeldev.site/en/articles/design-patterns-strategy > How the Strategy pattern encapsulates interchangeable algorithms and avoids fragile if/else chains, with a TypeScript calculator example. - [Home](/en/) - [Articles](/en/articles/) - Design Patterns: Strategy ## Design Patterns: Strategy If/else chains grow and turn fragile. Strategy isolates each algorithm behind a contract. May 5, 2022 - [Architecture](/en/articles/?topic=arquitetura) if/else chains grow and turn fragile. The Strategy pattern encapsulates each algorithm in its own class and lets implementations be swapped at runtime without changing the consuming code. ## The problem: endless if/else A calculator with addition, subtraction, multiplication, and division usually starts as a DefaultCalculator class: private methods per operation and a public function choosing which to invoke with a switch or if/else chain. class DefaultCalculator { public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; switch (operator) { case "*": return firstOperand * secondOperand; case "+": return firstOperand + secondOperand; case "-": return firstOperand - secondOperand; case "/": return firstOperand / secondOperand; case "**": return firstOperand ** secondOperand; case "%": return firstOperand % secondOperand; default: throw new Error("Operator not found!"); } } } The problem appears when the calculator must cover more binary operations between integers: percent, exponentiation, modulo, bit shifts. Each new feature changes the original implementation, raises coupling, and makes maintenance more expensive. ## What is the Strategy pattern? The pattern defines functionality through a contract (interface), implemented according to context. The interface defines the operation; concrete implementations define its execution. Consuming code depends on the abstraction. Each strategy stays isolated in its own class. New behaviors arrive without changing existing code, aligned with the Open/Closed Principle. ## Defining the contract The first step is the strategy contract interface. For the calculator, something receiving two numbers and returning the result: interface BinaryOperationParameters { firstOperand: number; secondOperand: number; operator: string; } type Result = number; interface BinaryOperationStrategy { calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result; } ## Implementing concrete strategies Each math operation becomes a class implementing BinaryOperationStrategy and executing a single operation. Addition and division: class Sum implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; return firstOperand + secondOperand; } } class Division implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; if (secondOperand === 0) { throw new Error("Division by zero is not allowed!"); } return firstOperand / secondOperand; } } ## The Context and the Factory To tie strategies together, a Context and a Factory (or Analyzer) come in. In ContextAnalyzer, a method evaluates the operator and returns the right Strategy: class ContextAnalyzer { public getInstance(operator: string): BinaryOperationStrategy { switch (operator) { case "*": return new Multiplication(); case "+": return new Sum(); case "-": return new Subtraction(); case "/": return new Division(); case "%": return new Percent(); case "**": return new Pow(); default: throw new Error("Operator not found!"); } } } The context receives and executes the strategy. It knows only the contract, not the concrete implementation. The old DefaultCalculator starts receiving this ContextAnalyzer by injection: class Calculator { constructor(private readonly contextAnalyzer: ContextAnalyzer) {} /** * The calculate implementation in Calculator does not change per operation. * What grows is the ContextAnalyzer, which adds one case per new operation. */ public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; return this.contextAnalyzer .getInstance(operator) .calculate({ firstOperand, secondOperand }); } } ## Why use Strategy? Strategy helps in legacy code with several business rules, each represented by an if and a long implementation. The shared part stays in the contract; each rule variation stays in its own class; a context analyzer (resolver/factory) picks the strategy. On each request, the code evaluates the operation context and selects the implementation matching the contract. ## Relation to other patterns - Adapter: Strategy varies behavior; Adapter isolates external dependencies behind an owned interface. - SOLID (OCP): Strategy is one way to apply the Open/Closed Principle. Strategy: contrato, seleção e concretas Calculator depende do contrato BinaryOperationStrategy. ContextAnalyzer escolhe a concreta (ex.: Sum) pelo operador; Sum, Division e Pow implementam o mesmo contrato. --- # Go intensive: concurrency, resilience, and distributed systems in 30 minutes URL: https://imrafaeldev.site/en/articles/go-intensive > A practical Go backend review covering goroutines, context, backpressure, idempotency, Kubernetes, observability, and performance. - [Home](/en/) - [Articles](/en/articles/) - Go intensive: concurrency, resilience, and distributed systems in 30 minutes ## Go intensive: concurrency, resilience, and distributed systems in 30 minutes A 30-minute guide to reactivate production Go knowledge for backend services, telemetry ingestion, and distributed systems. September 22, 2026 - [Architecture](/en/articles/?topic=arquitetura) - [Performance](/en/articles/?topic=performance) - [Messaging](/en/articles/?topic=mensageria) This is a review for backend engineers who already know Go and need to discuss or build production services again. The useful decisions are bounded concurrency, cancellation, finite queues, idempotency, and observability. The scope is Go 1.26. It is not a language introduction. Move quickly through syntax and spend time on the choices that shape a consumer, API, or telemetry pipeline. ## 30-minute route Time Topic Priority 0-4 min Types, structs, interfaces, errors Quick review 4-10 min Goroutines, channels, select, context High 10-17 min Bounded concurrency and backpressure Highest 17-23 min Resilient IoT pipeline Highest 23-26 min Runtime, memory, profiling High 26-30 min Architecture and interview answers Highest ## Production fundamentals Go favors composition, small contracts, and explicit flow. A value can validate itself without a framework: var ErrOutOfRange = errors.New("reading out of range") type Reading struct { DeviceID string Sequence uint64 Value float64 } func (r Reading) Validate() error { if r.DeviceID == "" { return errors.New("device_id is required") } if r.Value < -100 || r.Value > 250 { return fmt.Errorf("%w: %.2f", ErrOutOfRange, r.Value) } return nil } The zero value is often useful. Slices share a backing array until append reallocates; maps have no iteration order and need synchronization for concurrent access. Strings are immutable bytes, usually UTF-8. defer runs in LIFO order, while evaluating its arguments when registered. Interfaces are satisfied implicitly. Define a small interface where it is consumed. Errors are values: add context with %w and inspect the chain with errors.Is or errors.As. Reserve panic for broken invariants or unrecoverable startup failures. ## Goroutines, channels, and context A goroutine is not a dedicated OS thread. The runtime schedules it over OS threads. Every goroutine needs a clear owner, stop condition, and wait path. Channels move work or ownership. Mutexes protect shared state. A buffered channel smooths a temporary speed difference; it does not create unlimited capacity. func enqueue(ctx context.Context, jobs chan<- Reading, reading Reading) error { select { case jobs <- reading: return nil case <-ctx.Done(): return context.Cause(ctx) } } The producer closes a channel when it knows no more values will be sent. Sending to a closed channel or closing it twice panics. A nil channel blocks forever, and disables its select case. context.Context carries cancellation, deadlines, and request-scoped metadata. Receive it first, propagate it, call every returned cancel, and do not store it in a struct. Cancellation is cooperative: blocking loops must watch ctx.Done(). ## Bound concurrency before memory becomes the limit One goroutine per message becomes expensive when a downstream slows down. Queues and heap grow, GC gets busier, and the process may fail before CPU looks full. errgroup combines waiting, first-error propagation, and shared cancellation. Set a limit for independent tasks: func ProcessBatch(ctx context.Context, batch []Reading) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(16) for _, reading := range batch { reading := reading g.Go(func() error { return processOne(ctx, reading) }) } return g.Wait() } Classify errors before acting. A database outage can cancel a batch. Invalid, duplicate, or schema-incompatible messages belong in quarantine or a DLQ, not in a failure that stops the whole consumer. Use atomic for an independent flag or counter, sync.Mutex for an invariant across fields, and a channel for work transfer. Do not copy a mutex after first use or keep a lock during remote I/O. Backpressure is a product and operations policy. When ingestion accepts 50,000 messages per second and persistence completes 20,000, storing the difference in memory only moves the incident. Decide whether to block producers, reject with retry, pause consumption so the broker holds durable backlog, discard stale samples, aggregate, or spill to disk. Define queue size, occupancy metric, timeout, and saturation action. ## A resilient IoT pipeline device -> MQTT/broker -> Go ingestion -> stream -> processors -> storage \-> DLQ \-> current state MQTT fits device connectivity. A stream such as Kafka fits durable retention, replay, and internal partitioning. gRPC is typed internal RPC; WebSocket updates dashboards. These protocols solve different boundaries. Multiple workers break global ordering. Telemetry commonly needs order per device, so partition by a stable key such as hash(device_id) % N and process each partition sequentially. Keep both observed_at and ingested_at, plus sequence, event_id, and boot_id where applicable. Device clocks drift and restart. Treat the end-to-end path as *at least once*. Receive the event, validate its envelope and schema version, check an idempotency key, persist the effect and deduplication marker in one transaction where possible, then ACK. A transactional outbox closes the gap between committing database state and publishing a following event. Consumers still need idempotency because duplicates remain possible. Retry only transient failures. Invalid payloads and rejected business rules do not improve with another attempt. Add a limit, a total budget, and jitter so replicas do not retry together. At the edge, use TLS, per-device identities, topic authorization, strict payload limits, and validation before allocating large structures. Rotation, revocation, sequence numbers, nonces, and time windows matter when the business protocol must resist replay. ## Kubernetes, observability, and performance On SIGTERM, remove readiness, stop fetching work, drain in-flight work within the grace period, ACK only completed messages, and close producers, connections, and telemetry last. Liveness asks whether the process progresses; it should not depend on every external service. Readiness asks whether this pod can accept work now. For consumer autoscaling, CPU alone is weak. Watch lag, age of the oldest message, arrival rate, processing time, and worker-pool occupancy. Use structured logs and correlation fields without logging credentials or full sensitive payloads. Track throughput, errors by class, p50/p95/p99, lag, event age, retries, DLQ volume, duplicates, goroutines, heap, and GC pauses. Use sampled traces across ingestion, stream, and persistence; tracing every high-frequency reading can cost more than it helps. A data race is concurrent access to one memory location with at least one write and no synchronization order. Channel sends, mutex unlock/lock, and atomic operations create useful ordering. The detector only covers executed paths: go test -race ./... go test -bench=. -benchmem ./... go tool pprof cpu.out go tool trace trace.out G is a goroutine, M an OS thread, and P a logical execution resource. GOMAXPROCS limits Ps that run Go code concurrently, not the goroutine count. Goroutines are lightweight, not free. Profile before pooling or micro-optimizing. Preallocate known slice capacity, avoid repeated string/[]byte conversions in hot paths, and treat sync.Pool as an opportunistic temporary-object cache. ## Interview answers to keep ready **Is a goroutine a thread?** No. It is a lightweight runtime-managed execution unit multiplexed over OS threads. **Channel or mutex?** Use a channel to transfer work or ownership; use a mutex to protect shared state and invariants. **Who closes a channel?** The --- # Goroutines vs Event Loop: the wrong comparison between two concurrency models URL: https://imrafaeldev.site/en/articles/goroutines-vs-event-loop > Concurrency is not parallelism. When the Node.js Event Loop is enough for I/O and when Go goroutines fit CPU-bound load better. - [Home](/en/) - [Articles](/en/articles/) - Goroutines vs Event Loop: the wrong comparison between two concurrency models ## Goroutines vs Event Loop: the wrong comparison between two concurrency models Node.js with the Event Loop and Go with goroutines do not solve the same problem the same way. The common mistake is confusing concurrency with parallelism. June 24, 2026 - [Trade-offs](/en/articles/?topic=trade-offs) - [Performance](/en/articles/?topic=performance) The thesis is simple: Node.js with the Event Loop and Go with goroutines do not solve the same kind of problem the same way. The comparison goes bad when we treat both as direct competitors in every scenario. In practice, the most common mistake is confusing concurrency with parallelism. Concurrency is organizing several tasks that may be underway at the same time. Parallelism is actually executing work at the same time, using multiple CPU cores. That difference sounds academic until it shows up in production. The Node.js Event Loop is very good when the bottleneck is waiting: external APIs, databases, WebSockets, user input, queues, and events. While one operation waits for a response, the loop keeps serving other tasks. It is the corner-store owner at the counter: he does not stop because he asked someone to fetch candy from the stockroom. Goroutines, on the other hand, start looking more interesting when the work is CPU bound, divisible, and can use multiple cores with explicit concurrency control. They are lightweight execution units managed by the Go runtime. With them, a task can be broken into smaller parts, distributed, and synchronized at the end. It is more like a busy stall at the São João festival in Caruaru: one person grills corn, another stirs canjica, another slices bolo de rolo. Work advances at the same time, each person handling one part. ## Where Node.js starts to suffer I saw this very concretely in a commission-calculation process at a betting company. The application handled millions of bets per day, and part of the flow computed commission over several bet batches. At first, the Node.js process worked. Sequentially it was correct, but slow. When I tried to parallelize with the usual logic of several tasks at once, the limit appeared: the bottleneck was CPU. It was not just waiting on database, API, or external events. It was computation over a large bet buffer. That is the kind of scenario where Promise.all can mislead. It gives a sense of parallelism, but it does not automatically turn heavy CPU work into real parallel execution. If the tasks are compute-intensive and run on the same main thread, the Event Loop stays busy. The result can be worse than expected: loop blocking, higher latency, worse responsiveness, and more pressure on CPU and memory. The problem was not Node.js being bad. The problem was using the default Node model for a load that demanded another kind of execution. ## Where Go fit better The solution was rewriting that process in Go with goroutines. The idea was to split the calculation into smaller chunks, process those pieces in parallel, and synchronize only at the end. That design fit the problem better because the work was CPU bound and divisible. Instead of a centralized flow trying to coordinate several heavy operations, processing became distributed across smaller execution units. With a worker pool, for example, you can control the goroutine count, limit fan-out, use available cores better, and avoid firing unbounded work. The gain showed up. Total time dropped about 25%. The process that sat around 30 seconds started running near 22.5 seconds. CPU usage also improved. But the important part of the story is not “Go fixed it”. The important part is that Go fixed one side of the problem and revealed another. ## The bottleneck can move The first difficulty was guaranteeing the Go result equaled the Node.js result. That is less glamorous than talking about concurrency, but it is what separates real optimization from masked regression. If the calculation gets faster and changes the financial result, the improvement is worthless. After that, the main problem became chunk splitting. The initial strategy consumed too much memory. In local tests, with smaller datasets, proportional growth reached near 15% at some points. The discomfort came from a wrong expectation: I thought moving to Go would automatically solve the problem. In practice, I had only moved the bottleneck. Before, the limit was clearer on CPU. After, the partitioning strategy started pressuring memory. That can happen for several reasons: unnecessary copies, large buffers, slices holding references to bigger arrays, oversized internal queues, or too much work prepared before being processed. In the final result, memory still grew about 5%. In that case, the time and CPU gain compensated the loss. But that is not a universal rule. If the production load were much larger, or if the service ran with a thin memory margin, that trade-off could stop being acceptable. Parallelism costs coordination, allocation, synchronization, and observability. There is no free parallel execution. ## When I would keep Node.js I would keep Node.js without hesitation for I/O orchestration: WebSocket communication, calls to several APIs, database queries, event dispatch, integration between services, and flows where dead time is waiting. In those cases, the Event Loop is an excellent choice. It allows high volumes of concurrent operations without creating one thread per request. For event-oriented applications, that is simple, productive, and easy to fit into the JavaScript ecosystem. The mistake is pushing that same model onto heavy computation and thinking I/O concurrency becomes CPU parallelism. It does not. ## When I would look at Go I would start looking at Go when the task is clearly CPU bound: high-volume data computation, batch image processing, heavy aggregations, compression, large buffer transforms, simulations, or any routine where the machine spends more time computing than waiting for external responses. In that kind of scenario, goroutines with a worker pool give better control over CPU use. They also make the separation between work units, synchronization, and result collection more explicit. But Go also charges a price. You must think about chunk granularity, memory consumption, cancellation, error handling, backpressure, worker limits, contention, and result consistency. If the work split is naive, the CPU gain can arrive with memory blowups or needless complexity. ## Counterargument: Node.js also has worker threads There is a fair counterargument: Node.js is not limited to the Event Loop for everything. Worker threads exist precisely to run heavy work outside the main thread. There are also strategies with queues, separate processes, helper services, and native addons. So the honest comparison is not “Node.js cannot”. It can. The question is implementation cost, team maturity, observability, integration with the existing system, and how much effort is worth investing to keep that processing inside the Node ecosystem. On some teams, worker threads may be enough and cheaper than introducing Go. On others, splitting CPU-bound processing into a Go service may be simpler to operate and scale. The decision should not come from language preference. It should come from the nature of the load. ## The practical rule that stuck After that case, the rule that stuck for me is this: if the problem is waiting on many things at once, Node.js with the Event Loop tends to orchestrate very well. If the problem is computing many things at once, I consider Go with goroutines earlier. But I also grew suspicious of migrations promising to fix everything. Switching technology may only move the bottleneck. In my case, CPU and time improved, but memory and chunking had to be reanalyzed. In the end, the goroutines vs Event Loop fight is less about which model --- # Articles URL: https://imrafaeldev.site/en/articles > Engineering patterns and decisions in technical prose, with code. - [Home](/en/) - Articles ## Articles Engineering patterns and decisions in technical prose, with code. AllPerformanceArchitectureTrade-offsMessaging - ## [Go intensive: concurrency, resilience, and distributed systems in 30 minutes](/en/articles/go-intensive/) September 22, 2026 [Architecture](/en/articles/?topic=arquitetura) - [Performance](/en/articles/?topic=performance) - [Messaging](/en/articles/?topic=mensageria) A 30-minute guide to reactivate production Go knowledge for backend services, telemetry ingestion, and distributed systems. - ## [Derusting logic #02: Container With Most Water](/en/articles/derusting-logic-container-with-most-water/) September 15, 2026 [Performance](/en/articles/?topic=performance) - [Trade-offs](/en/articles/?topic=trade-offs) I tried to pick the next step by looking only at the neighbors. It worked in some cases, but the problem asked for a wider view. - ## [Derusting logic #01: Group Anagrams](/en/articles/derusting-logic-group-anagrams/) August 17, 2026 [Trade-offs](/en/articles/?topic=trade-offs) - [Performance](/en/articles/?topic=performance) I had not stopped writing code. What changed was outsourcing parts of the reasoning. Group Anagrams was the exercise to recover the habit. - ## [Most backend performance problems start close to the data](/en/articles/backend-performance-close-to-data/) July 16, 2026 [Performance](/en/articles/?topic=performance) - [Architecture](/en/articles/?topic=arquitetura) Slow API and the meeting already fills with solutions. I start close to the data — not to blame the database, but because that check usually gives a fast signal. - ## [Goroutines vs Event Loop: the wrong comparison between two concurrency models](/en/articles/goroutines-vs-event-loop/) June 24, 2026 [Trade-offs](/en/articles/?topic=trade-offs) - [Performance](/en/articles/?topic=performance) Node.js with the Event Loop and Go with goroutines do not solve the same problem the same way. The common mistake is confusing concurrency with parallelism. - ## [TypeScript Clean Architecture: Core, Adapters, and Infra](/en/articles/typescript-clean-architecture/) March 15, 2023 [Architecture](/en/articles/?topic=arquitetura) Weak architecture blocks maintenance, testing, and change. This Clean Architecture derivation for TypeScript backends separates Core, Adapters, and Infra — with dependencies pointing inward. - ## [Design Patterns: Strategy](/en/articles/design-patterns-strategy/) May 5, 2022 [Architecture](/en/articles/?topic=arquitetura) If/else chains grow and turn fragile. Strategy isolates each algorithm behind a contract. - ## [Stop being hostage to dependencies. Say hello to the Adapter design pattern](/en/articles/design-patterns-adapter/) April 26, 2022 [Architecture](/en/articles/?topic=arquitetura) Migrating from MySQL to PostgreSQL does not require rewriting the service. The Adapter isolates the plugin behind a contract the business rule understands. --- # TypeScript Clean Architecture: Core, Adapters, and Infra URL: https://imrafaeldev.site/en/articles/typescript-clean-architecture > A Clean Architecture derivation for TypeScript backends: Core with usecases and protocols, bidirectional Adapters, and NestJS Infra with dependency injection. - [Home](/en/) - [Articles](/en/articles/) - TypeScript Clean Architecture: Core, Adapters, and Infra ## TypeScript Clean Architecture: Core, Adapters, and Infra Weak architecture blocks maintenance, testing, and change. This Clean Architecture derivation for TypeScript backends separates Core, Adapters, and Infra — with dependencies pointing inward. March 15, 2023 - [Architecture](/en/articles/?topic=arquitetura) Software development changes all the time. Weak architecture becomes expensive maintenance, slow features, hard tests, and bugs that are hard to isolate. It is worth investing in a structure that supports evolution without rewriting the system at every business pressure. ## A bit of history Clean Architecture is the name Robert C. Martin (Uncle Bob) gave, in 2012, in the book *Clean Architecture: A Craftsman’s Guide to Software Structure and Design*. The proposal avoids the rigidity of architectures coupled to framework and database: the core stays stable; external details change. The idea draws from DDD, SOLID, Onion Architecture, and Hexagonal Architecture. ## General proposal This article describes Clean Architecture and a practical derivation for TypeScript backends: three layers — **Core**, **Adapters**, and **Infra**. - **Core** — business rules and domain entities. Innermost layer. - **Infra** — external connections: concrete repositories, REST controllers, DI modules, framework boilerplate. - **Adapters** — mediation in both directions. A controller does not call a “raw” usecase: it goes through a service. A usecase does not talk to the database: it talks to a protocol that an adapter (repository, connector, handler) implements. Each layer has different capabilities and constraints; SOLID weighs more in Core. It works for HTTP CRUD and for systems with several frameworks and channels. Concrete benefits: clear responsibilities (reading and maintenance), flexibility to swap plugins without rewriting rules, and isolated tests per layer. ## Layer guide Example: user CRUD over REST with NestJS. Installation details are out of scope. **Core-to-infra** writing (inside out). ## Core In the classic design, *domain* and *entities* sit very close. Here they form the **Core**: everything the business rule *is* — features and domain representations. In the example, the main entity is User (id, name), in core/entities. ### Entities // core/entities/UserEntity.ts export interface UserEntityProps { id?: string; name: string; } export class UserEntity { constructor(private readonly props: UserEntityProps) {} get id(): string { return this.props.id ?? ""; } get name(): string { return this.props.name; } } The entity receives typed props and exposes getters. It depends on an interface any transfer DTO can satisfy later. ### Features and usecases CRUD needs create, fetch, update, and remove. In Core, each usecase implements a contract (feature) with a single public method — aligned with Liskov, open/closed, interface segregation, and single responsibility. The usecase does **not** access the database: it knows **protocols** describing the external action (dependency inversion). Registration: name is required; if it already exists, error; otherwise return UserEntity. - contract CreateUser - implementation CreateUserUsecase In TypeScript, an abstract class with abstract methods works as both contract *and* value — useful for DI (const createUserSymbol = CreateUser): // core/features/CreateUser.ts export abstract class CreateUser { abstract execute(name: string): Promise<UserEntity>; } // core/usecases/CreateUserUsecase.ts export class CreateUserUsecase implements CreateUser { constructor( private readonly createUserProtocol: CreateUserProtocol, private readonly getByNameProtocol: GetUserByNameProtocol, ) {} async execute(name: string): Promise<UserEntity> { const existsName = await this.getByNameProtocol.getByName(name); if (existsName) { throw new UserAlreadyExistsException( `the name ${name} already exists`, ); } return this.createUserProtocol.register(name); } } The usecase defines *what* (validate name, register). It does not define *how* to fetch or persist. The rule stays independent of lib, framework, and database. Watch out: a usecase that only delegates to the protocol without validating may be pushing business rules into the adapter. In CreateUserUsecase, the duplicate-name check is Core’s obligation. ### Exceptions UserAlreadyExistsException belongs to Core: an invalid rule flow is also a rule. Each failure mapped to a known exception helps maintenance. Base with code (later becomes HTTP status at the edge): // core/exceptions/IBaseException.ts export abstract class IBaseException extends Error { code: number; constructor(message: string) { super(message); } } // core/exceptions/UserAlreadyExistsException.ts export class UserAlreadyExistsException extends IBaseException { constructor(message?: string) { super(message ?? "User already exists"); this.code = 400; } } The usecase **throws** exceptions; it does **not** handle them. Mapping unknown type → known type belongs in adapter or infra. ### Protocols CreateUserProtocol and GetUserByNameProtocol are contracts for external-device access. A protocol exists to inform or trigger external action — **not** to process business rules. Preference: one public method per protocol. // core/protocols/CreateUserProtocol.ts export abstract class CreateUserProtocol { abstract register(name: string): Promise<UserEntity>; } // core/protocols/GetUserByNameProtocol.ts export abstract class GetUserByNameProtocol { abstract getByName(name: string): Promise<UserEntity | null>; } Core is the center; the adaptation layer connects the rest. ## Adapter Adapters control bidirectional traffic: external → rule and rule → external. They adapt objects, parameters, and exceptions — the same spirit as the [Adapter pattern](/en/articles/design-patterns-adapter/). Two groups: - Called by Core — implement at least one protocol. - Called by Infra — generally **services**. ### Connectors, handlers, and repositories Classes implementing protocols. Each adapts **one** external device (ORM, HTTP client, queue, filesystem). Naming convention: - **Repositories** — protocol tied to a database (familiar vocabulary). - **Connectors** — return data without being a “table” (e.g. ClientHttpFetchConnector, ClientHttpAxiosConnector). - **Handlers** — process without synchronous return (e.g. publishing to Kafka). Other names are valid; the criterion is one adapter per device. In the CRUD, only repository (mock): // adapters/repositories/UsersMockRepository.ts export class UsersMockRepository implements GetUserByIdProtocol, GetUserByNameProtocol, CreateUserProtocol, UpdateUserProtocol, DeleteUserProtocol { private db: DbConnector; constructor() { this.db = mockDbConnector; } async getById(id: string): Promise<UserEntity> { return this.db.users.getById(id); } async getByName(name: string): Promise<UserEntity | null> { return this.db.users.getByName(name); } async register(name: string): Promise<UserEntity> { return this.db.users.register(name); } async update(id: string, name: string): Promise<UserEntity> { return this.db.users.update(id, name); } async delete(id: string): Promise<void> { return this.db.users.delete(id); } } Mock connector: export const mockDbConnector: DbConnector = { users: { getById: async (id: string) => Promise.resolve(new UserEntity({ id, name: "Test" })), getByName: async (name: string) => Promise.resolve(new UserEntity({ id: "1", name })), register: async (name: string) => Promise.resolve(new UserEntity({ id: "2", name })), update: async (id: string, name: string) => Promise.resolve(new UserEntity({ id, name })), delete: async (_id: string) => Promise.resolve(), }, profiles: { getById: async (_id: string) => Promise.resolve(null), getByName: async (_name: string) => Promise.resolve(null), register: async (_name: --- # VBET: analytics on top of a SQL Server we could not change URL: https://imrafaeldev.site/en/cases/external-sql-server-analytics > Commission dashboard at VBET: external SQL Server, owned ETL, and cache. The measured line went from about seven minutes at peak to under one second with a warm cache. - [Home](/en/) - [Cases](/en/cases/) - VBET: analytics on top of a SQL Server we could not change VBET ## VBET: analytics on top of a SQL Server we could not change The database belonged to another team. The dashboard had to stop depending on a schema we did not control. Senior Backend Engineer Oct/2023 – Feb/2025 ~7 min → <1 s commissions, from the original load to the warm cache Pipeline de comissões Cada estágio corresponde a uma decisão incremental documentada no case. Números de outras histórias não entram neste desenho. - [01Context](#context) - [02Constraints](#constraints) - [03Problem](#problem) - [04Decision](#decision) - [05Discarded alternative](#discarded-alternative) - [06Result](#result) - [07Limitations](#limitations) ## Context At VBET, between October 2023 and February 2025, the analytics product served iGaming influencers and affiliates. The dashboard gathered dozens of metrics; commission was the most critical reading. Influencers accepted a small lag in same-day data as long as the screen responded. Payout depended on consolidated prior-day data, not on the live value. ## Constraints The SQL Server was external, shared, and not reliably modifiable. Temporary indexes could be removed by the database owner. The original API mixed SQL queries built from parameters, with injection risk, and aggregated too much in memory. ## Problem The system had been sized for smaller influencers. With larger bases, the worst dashboard peak reached about seven minutes. Security and maintainability came before performance: raw queries, little test coverage, and a synchronous path that recalculated too much on every request. ## Decision The evolution was incremental, in the order the constraints appeared: - remove unsafe SQL, parameterize access, document, and test; - optimize queries and temporary indexes, as mitigation rather than invariant; - parallelize independent queries with Go, goroutines, and channels; - when the bottleneck moved back to SQL Server, build an owned ETL and PostgreSQL, with pre-computation, checkpoints, and reconciliation; - separate REALTIME reads (trend, eventual consistency) from CLOSED reads (financial accuracy and payout); - cache-aside with TTL aligned to the accepted lag of about five minutes; - controlled degradation if the cache failed, instead of taking the screen down. ## Discarded alternative Insisting on indexes in the external database as architecture, or recalculating years of history on every access. Also discarded: paying affiliates from REALTIME data. ## Result The reconciled line in the experience dossier, for the commission/dashboard path, is: - initial worst peak: about 7 minutes; - after queries and indexes: about 3 minutes; - after parallelization: about 1 minute; - after ETL/PostgreSQL: about 15 seconds at commission p99 without cache; - warm cache: under 1 second. Each number belongs to its stage. It does not describe the gain of a later decomposition into microservices. ## Limitations The investigation, the ETL + owned database decision, the REALTIME/CLOSED split, and the degradation policy are the attributable core here. Decomposing the monolith into Kubernetes is a separate story and does not mix the “500%” nor sub-60 ms latency into this case. Older resume versions citing 30 seconds on cold load, 11 seconds, or SLA percentages without a scenario are left out. If the product starts requiring realtime accuracy for payout, the CLOSED split stops being enough. Contact ## Dealing with a system that's stopped being simple? Reach out directly via @imrafaeldev, no form — for professional conversation, start on LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Open the contact page](/en/contact/) --- # Case studies URL: https://imrafaeldev.site/en/cases > Each case documents the constraint, the decision, the discarded alternative, and the measured outcome. It also states when to revisit the decision. - [Home](/en/) - Cases ## Case studies Each case documents the constraint, the decision, the discarded alternative, and the measured outcome. It also states when to revisit the decision. - Infosistemas Feb/2025 – May/2026 ## [Infosistemas: a failure contract for RabbitMQ messaging](/en/cases/rabbitmq-messaging/) Intermittent failures between microservices with no contract for retry, DLQ, or duplication. Adding more consumers only pushed the overload elsewhere. **~98%** fewer intermittent failures in critical flows [Open the case — Infosistemas: a failure contract for RabbitMQ messaging](/en/cases/rabbitmq-messaging/) - VBET Oct/2023 – Feb/2025 ## [VBET: analytics on top of a SQL Server we could not change](/en/cases/external-sql-server-analytics/) The database belonged to another team. The dashboard had to stop depending on a schema we did not control. **~7 min → <1 s** commissions, from the original load to the warm cache [Open the case — VBET: analytics on top of a SQL Server we could not change](/en/cases/external-sql-server-analytics/) - Flapper Sep/2021 – Jun/2022 ## [Flapper: discovering the domain before splitting the monolith](/en/cases/undocumented-monolith-modernization/) The product could not stop, and the original authors were already gone. Before migrating, we had to discover which boundaries the database still revealed. **~25%** fewer tables in the domain-based split [Open the case — Flapper: discovering the domain before splitting the monolith](/en/cases/undocumented-monolith-modernization/) --- # Infosistemas: a failure contract for RabbitMQ messaging URL: https://imrafaeldev.site/en/cases/rabbitmq-messaging > RabbitMQ messaging redesign at Infosistemas with durable queues, DLQ, retry, idempotency, and prefetch. The work reduced intermittent failures between microservices by about 98%. - [Home](/en/) - [Cases](/en/cases/) - Infosistemas: a failure contract for RabbitMQ messaging Infosistemas ## Infosistemas: a failure contract for RabbitMQ messaging Intermittent failures between microservices with no contract for retry, DLQ, or duplication. Adding more consumers only pushed the overload elsewhere. Senior Software Engineer / Software Architect Feb/2025 – May/2026 ~98% fewer intermittent failures in critical flows Contrato de falha na mensageria Publicação confirmada, consumo com prefetch controlado, retry com backoff e DLQ por fluxo. Escalar só consumidores fica de fora do desenho. - [01Context](#context) - [02Constraints](#constraints) - [03Problem](#problem) - [04Decision](#decision) - [05Discarded alternative](#discarded-alternative) - [06Implementation](#implementation) - [07Result](#result) - [08Limitations](#limitations) ## Context Infosistemas operates management platforms for rental companies, fleets, and automakers. The work happened in the architecture team, in collaboration with DevOps, SREs, and DBAs, between February 2025 and May 2026. This case covers the messaging track. Other tracks from the same experience (ERP security, signing journeys, fiscal integrations) exist in the sources but are not included here as extra numbers or claims. ## Constraints Critical flows crossed microservices. Failure was intermittent: the same operation could complete on one run and fail on the next. Raising concurrency or prefetch without criteria transferred overload to consumers, services, or downstream databases. ## Problem Messages stopped completing the expected flow. Investigating partial failure was hard. There was no explicit contract for temporary failure, permanent failure, duplication, or poison messages. ## Decision Messaging was redesigned to make failure behavior predictable: - durable queues; - per-flow DLQ, so a message that must neither disappear nor repeat without control has a place to go; - retry with backoff for temporary unavailability; - idempotency and deduplication in the consumer, because duplicated delivery must not repeat a business effect; - publisher confirms, to reduce uncertainty at publish time; - prefetch tuning, instead of opening concurrency indiscriminately. ## Discarded alternative Treating the problem as a capacity shortage (more consumers, more prefetch) without changing the failure contract. That would move the bottleneck and keep silent loss or duplication. ## Implementation The redesign applied these mechanisms to the critical flows: confirmed publishing, durable queues with controlled prefetch, idempotent consumers, retry with backoff, and per-flow DLQ. Operations gained a predictable path for temporary failure and for permanent failure, instead of relying on ad hoc reprocessing. ## Result The recorded reduction in intermittent failures in critical flows between microservices was approximately 98%. The number describes those flows after the redesign, not the entire company operation nor other tracks. ## Limitations Metrics from other tracks that are still pending method or confirmation are left out. If volume or the microservice map changes so that DLQ and prefetch no longer isolate failure, tuning must be revisited with queue and consumer telemetry. Contact ## Dealing with a system that's stopped being simple? Reach out directly via @imrafaeldev, no form — for professional conversation, start on LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Open the contact page](/en/contact/) --- # Flapper: discovering the domain before splitting the monolith URL: https://imrafaeldev.site/en/cases/undocumented-monolith-modernization > Incremental modernization of an undocumented PHP monolith at Flapper: database as discovery source, bounded contexts, and Strangler. The domain-based split reduced the table count by approximately 25%. - [Home](/en/) - [Cases](/en/cases/) - Flapper: discovering the domain before splitting the monolith Flapper ## Flapper: discovering the domain before splitting the monolith The product could not stop, and the original authors were already gone. Before migrating, we had to discover which boundaries the database still revealed. Full Stack Software Engineer Sep/2021 – Jun/2022 ~25% fewer tables in the domain-based split Descoberta de domínio antes da migração O banco legado revela fronteiras; pessoas, autenticação e aeronaves saem gradualmente para contextos com persistência própria. - [01Context](#context) - [02Constraints](#constraints) - [03Problem](#problem) - [04Decision](#decision) - [05Discarded alternative](#discarded-alternative) - [06Result](#result) - [07Limitations](#limitations) ## Context At Flapper, the core product was a PHP monolith over seven years old, with little useful documentation and without the developers who had created it. The application sustained the executive aviation operation and could not be stopped for a rewrite. ## Constraints Code and database accumulated rules and dependencies that were hard to explain. Changes had unpredictable side effects, and there were no remaining specialists to confirm how each part of the system should evolve. Migration had to coexist with the product in production. ## Problem Switching PHP for another technology would not answer the main question: which rules belonged together and which dependencies could be separated without breaking operations. The system needed domain boundaries before new services. ## Decision I used the existing database and code as a discovery source. Table groupings and relationships helped identify bounded contexts; from there, migration followed the Strangler pattern: - extract one domain at a time, without stopping the monolith; - keep in each context only the local representation of the data it needed; - propagate changes through Kafka events, instead of connecting every service to the old database; - use Node.js, NestJS, and Go in the first modules, with gRPC, REST, or GraphQL depending on the consumer; - document the strategy and the first modules so the team could continue the transformation. ## Discarded alternative Rewriting the whole monolith, or keeping new services tied to the same database and shared relationships. The first option would stop the business; the second would preserve the coupling the migration needed to reduce. ## Result The domain-based split reduced the table count by approximately 25% and created seven databases organized by context. The people, authentication, and aircraft modules were the first steps of a transformation planned to continue beyond the initial delivery. ## Limitations The number measures the table reduction in that domain split, not financial gain, the complete migration, or a market result for the company. The experience end date diverges across older sources; the published period follows the exported profile. If a domain still depends on rules unmapped in the monolith, its extraction must be postponed or given an explicit transitional integration. Contact ## Dealing with a system that's stopped being simple? Reach out directly via @imrafaeldev, no form — for professional conversation, start on LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Open the contact page](/en/contact/) --- # Contact URL: https://imrafaeldev.site/en/contact > Talk to Rafael Pereira via @imrafaeldev on LinkedIn, GitHub, Instagram, and YouTube. No form: pick a channel and reach out directly. - [Home](/en/) - Contact Contact ## Dealing with a system that's stopped being simple? Reach out directly via @imrafaeldev — no form. For professional conversation, start on LinkedIn; to see decisions in code, head to GitHub. - [Instagram @imrafaeldev](https://www.instagram.com/imrafaeldev/) - [YouTube @imrafaeldev](https://www.youtube.com/@imrafaeldev) - [GitHub @imrafaeldev](https://github.com/imrafaeldev) - [LinkedIn @imrafaeldev](https://www.linkedin.com/in/imrafaeldev/) --- # Azify URL: https://imrafaeldev.site/en/experience/azify > My consulting work at Azify with settlement, BaaS, and financial services. - [Home](/en/) - Azify ## Azify My consulting work at Azify with settlement, BaaS, and financial services. Role Senior Backend Engineer — Consulting Period Mar 2025 to Jun 2025 ## Context At Azify, I worked as a consultant on financial infrastructure for fintechs and smaller banks. The product covered capabilities such as Pix, transfers, cards, digital wallets, and other banking services. ## How I worked I contributed to architectural decisions and helped establish engineering practices for an environment where consistency, security, and stability had direct financial impact. I built a NestJS settlement engine with multi-exchange integrations, risk monitoring, and compliance controls. I also worked on exchange and blockchain integrations for transactional flows and on a multi-tenant BaaS platform using OAuth 2.0, JWT, and encryption. Through query profiling, index review, and Redis optimization, I reduced latency in critical financial APIs by 30%. ## What I took from it This consulting work brought recurring parts of my trajectory, including payments, multi-tenancy, and transactional systems, into a setting with even greater responsibility for authorization and consistency. --- # Braistech URL: https://imrafaeldev.site/en/experience/braistech > My experience at Braistech with product, microservices, and crypto assets. - [Home](/en/) - Braistech ## Braistech My experience at Braistech with product, microservices, and crypto assets. Role Mid-level Full Stack Software Engineer Period Nov 2019 to Jan 2021 ## Context At Braistech, I had one of my first product experiences in a small environment with a distributed level of responsibility. The domain involved crypto-asset contracts and financial movements. ## How I worked I led the structure of the main system with Node.js and NestJS, took part in designing microservices for the business core, and developed Flutter applications. I also built a contract system and worked on payment integrations related to the Binance ecosystem. I participated in a transition from an MVC organization toward Clean Architecture. The goal was to reduce coupling and make a growing system easier to maintain, while I guided junior developers through the code decisions. ## What I took from it This stage consolidated my interest in backend and architecture. Working across the full product also gave me a full-stack perspective that remains useful in conversations with frontend and product teams. --- # EDS and Rio de Janeiro Civil Police URL: https://imrafaeldev.site/en/experience/eds-policia-civil-rio > My consulting experience on sensitive public systems. - [Home](/en/) - EDS (Civil Police of Rio de Janeiro) ## EDS (Civil Police of Rio de Janeiro) My consulting experience on sensitive public systems. Role Backend Engineer — Consulting Period Jul 2025 to Dec 2025 ## Context In my consulting work for EDS, I worked on systems for the Civil Police of Rio de Janeiro. The context involved a critical public operation, a high-volume health management system, and an evolving legal ERP. ## How I worked I structured the health system backend with NestJS and SQL Server. I also refactored legacy routes and contributed to flows for process automation, document management, and evidence collection. Security, access control, traceability, and LGPD compliance guided how every route had to evolve. Beyond backend work, I collaborated on shared design-system components to align API contracts with the interfaces used in the operation. ## What I took from it The work reinforced the care needed to evolve sensitive systems without losing auditability. Rather than separating security from delivery, I treated access and traceability as part of the product contract. --- # Flapper URL: https://imrafaeldev.site/en/experience/flapper > My experience at Flapper with incremental modernization of a production legacy system. - [Home](/en/) - Flapper ## Flapper My experience at Flapper with incremental modernization of a production legacy system. Role Full Stack Software Engineer Period Sep 2021 to Jun 2022 ## Context At Flapper, I worked on an executive aviation platform whose main product was a PHP monolith more than seven years old, with little useful documentation and no original developers available to explain the system. ## How I worked The challenge was not to replace PHP with TypeScript. The application was in production and sustained the business, so I began with domain discovery. I used the database to map relationships, identify bounded contexts, and plan an incremental migration based on the Strangler pattern. I migrated modules such as people, authentication, and aircraft to Node.js, NestJS, and Go services. To reduce relational dependencies between contexts, we worked with local projections and Kafka events; gRPC, REST, and GraphQL were used according to each integration’s needs. ## What I took from it The experience consolidated my view of legacy modernization: technology comes after understanding boundaries, risks, and a sequence that preserves the operation. Alongside module delivery, I documented decisions and led workshops so the team could continue the transformation. ## Related case study The case study shows how I investigated the domain from the database and code and led incremental monolith modernization without interrupting the operation. [Read the full case study](/en/cases/undocumented-monolith-modernization/) --- # Infosistemas URL: https://imrafaeldev.site/en/experience/infosistemas > My experience at Infosistemas with messaging, integrations, and mobility platforms. - [Home](/en/) - Infosistemas ## Infosistemas My experience at Infosistemas with messaging, integrations, and mobility platforms. Role Senior Software Engineer / Software Architect Period Feb 2025 to May 2026 ## Context At Infosistemas, I worked on platforms for rental companies, fleets, automakers, and mobility operations. It was an enterprise environment of integrations, fiscal flows, and high-volume services, where I worked closely with DevOps, SREs, and DBAs. ## How I worked My work combined architecture with hands-on delivery. I redesigned flows between microservices, led NestJS and Go integrations, and implemented critical-event traceability with NestJS and MongoDB. I also evolved APIs, investigated security issues, and contributed to digital journeys and webapp components when backend and interface continuity was needed. The principle that guided my work was making failures observable and manageable from the design stage. In messaging, I treated durability, retry, idempotency, and consumer control as part of the flow rather than later fixes. ## What I took from it This experience expanded my work in systems with many dependencies and specialists. I learned to turn requirements, risks, and operational constraints into decisions that stayed clear through client validation. ## Related case study The case study details how I redesigned RabbitMQ messaging to make critical flows more predictable and reduce intermittent failures between microservices. [Read the full case study](/en/cases/rabbitmq-messaging/) --- # Maxmilhas URL: https://imrafaeldev.site/en/experience/maxmilhas > My experience at Maxmilhas with post-sale automation and legacy integration. - [Home](/en/) - Maxmilhas ## Maxmilhas My experience at Maxmilhas with post-sale automation and legacy integration. Role Full Stack Software Engineer Period Apr 2023 to Oct 2023 ## Context At Maxmilhas, I worked in a short and intense period on travel post-sale flows involving cancellations, rebooking, coupons, and customer communication. ## How I worked I built Node.js, NestJS, and Elixir microservices to automate processes that still depended on support intervention. I implemented eligibility, expiry, and accumulation rules for coupons, along with calculations and validations for cancellation and rebooking flows. Together, those automations reduced the need for manual intervention by 34%. Another challenge was integrating a PHP 5.7 monolith, more than ten years old, with the commercial CRM without putting the operational core at risk. I also evolved flight monitoring, notifications, and proactive customer messages. ## What I took from it I learned to prioritize small, reversible interventions when outcomes need to arrive quickly. Instead of proposing a broad transformation, I focused on points that released operational work. --- # South System, QUIQ, and Itaú URL: https://imrafaeldev.site/en/experience/south-system-quiq-itau > My experience with a white-label, multi-tenant marketplace for financial institutions. - [Home](/en/) - South System (assigned to QUIQ/Itaú) ## South System (assigned to QUIQ/Itaú) My experience with a white-label, multi-tenant marketplace for financial institutions. Role Backend Engineer Period Jun 2022 to Apr 2023 ## Context At South System, I was assigned to QUIQ to work on a Marketplace as a Service for financial institutions. Itaú was the first context, but the product needed to accept new banks without requiring a fork for each client. ## How I worked I participated in architecture, data modeling, technology choices, and rule refinement with product. The result was a white-label, multi-tenant platform with logical tenant isolation and Hexagonal Architecture to keep the domain apart from specific integrations. I worked with Node.js, TypeScript, MySQL, asynchronous Go services, and AWS. I structured unit and integration testing for critical cases and used static analysis as part of the quality workflow. ## What I took from it This experience changed how I communicate architecture. I started treating alignment with product, the Product Owner, and stakeholders as part of the technical decision rather than a later step after code. --- # Sustentec URL: https://imrafaeldev.site/en/experience/sustentec > My experience at Sustentec with research systems, APIs, and quality. - [Home](/en/) - Sustentec ## Sustentec My experience at Sustentec with research systems, APIs, and quality. Role Mid-level Full Stack Software Engineer Period Jan 2021 to Aug 2021 ## Context At Sustentec, I worked on systems connected to laboratories, research, and development. The work combined maintaining an existing product with evolving its features and integrations. ## How I worked I developed a REST API in Dart with Shelf to integrate research-institution databases. I also maintained and evolved a laboratory management system with Java, Spring Boot, JPA, Hibernate, PostgreSQL, and Angular. I added integration tests where that coverage did not exist, developed reports and end-to-end features, and took part in gathering requirements with clients and refining sprints with the Product Owner. ## What I took from it This experience reinforced that quality is not limited to unit tests. In a system with several layers, I needed to validate the actual behavior across API, persistence, and interface. --- # VBET URL: https://imrafaeldev.site/en/experience/vbet > My experience at VBET with security, analytics, and performance at scale. - [Home](/en/) - VBET ## VBET My experience at VBET with security, analytics, and performance at scale. Role Senior Backend Engineer Period Oct 2023 to Feb 2025 ## Context At VBET, I worked on an analytics product for iGaming affiliates and influencers. It calculated financial and operational metrics in a platform that began serving audiences much larger than originally expected. ## How I worked Before addressing performance, I reduced security and maintenance risks in the legacy API. I replaced unsafe queries, organized the codebase around Clean Architecture and dependency injection, and established tests and documentation to support the following changes. I then addressed performance in stages. I used Go, goroutines, channels, and parallel queries to reduce the first commission-calculation stage from about seven to three minutes. Since the SQL Server was external and could not be changed, I designed an ETL with checkpoints, pre-calculated aggregates, and reconciliation, separating provisional data from consolidated data. ## What I took from it This experience consolidated how I make performance decisions: understand the actual constraint, accept the consistency appropriate to each use, and then choose the technology that addresses it. ## Related case study The case study follows the analytics dashboard evolution: from API risk reduction to ETL, pre-computation, reconciliation, and caching over an external SQL Server. [Read the full case study](/en/cases/external-sql-server-analytics/) --- # Rafael Pereira, senior software engineer URL: https://imrafaeldev.site/en > Institutional portfolio and editorial hub for Rafael Pereira. I work at the points where simple systems stop being simple. Rafael Pereira / software engineer / systems ## I work at the points where simple systems *stop being simple*. Case studies, projects, and technical writing on scale, failure, legacy systems, and business rules. Each piece shows the constraint, the decision, and where the solution stops. [See the case studies](/en/cases/) [See contact options](/en/contact/) [Case in focus **Infosistemas** ~98% · fewer intermittent failures in critical flows Open the case](/en/cases/rabbitmq-messaging/) [**VBET** ~7 min → <1 s](/en/cases/external-sql-server-analytics/) - [~98% Infosistemas: fewer intermittent failures in critical flows](/en/cases/rabbitmq-messaging/) - [~7 min → <1 s VBET: commissions, from the original load to the warm cache](/en/cases/external-sql-server-analytics/) - [~25% Flapper: fewer tables in the domain-based split](/en/cases/undocumented-monolith-modernization/) Signal Method ## The constraint comes before the diagram. I start with what happens when a message fails. The database cannot change. The legacy core cannot stop. The happy path comes later. The decision records the discarded alternative and the condition that would justify revisiting it. Code shows what was built; the case shows why. Proof ## Three systems, three constraints Infosistemas Feb/2025 – May/2026 ~98% fewer intermittent failures in critical flows ### [Infosistemas: a failure contract for RabbitMQ messaging](/en/cases/rabbitmq-messaging/) Intermittent failures between microservices with no contract for retry, DLQ, or duplication. Adding more consumers only pushed the overload elsewhere. [Open the case](/en/cases/rabbitmq-messaging/) Contrato de falha na mensageria Publicação confirmada, consumo com prefetch controlado, retry com backoff e DLQ por fluxo. Escalar só consumidores fica de fora do desenho. [Ver o caso](/en/cases/rabbitmq-messaging/) VBET Oct/2023 – Feb/2025 ~7 min → <1 s commissions, from the original load to the warm cache ### [VBET: analytics on top of a SQL Server we could not change](/en/cases/external-sql-server-analytics/) The database belonged to another team. The dashboard had to stop depending on a schema we did not control. [Open the case](/en/cases/external-sql-server-analytics/) Pipeline de comissões Cada estágio corresponde a uma decisão incremental documentada no case. Números de outras histórias não entram neste desenho. [Ver o caso](/en/cases/external-sql-server-analytics/) Flapper Sep/2021 – Jun/2022 ~25% fewer tables in the domain-based split ### [Flapper: discovering the domain before splitting the monolith](/en/cases/undocumented-monolith-modernization/) The product could not stop, and the original authors were already gone. Before migrating, we had to discover which boundaries the database still revealed. [Open the case](/en/cases/undocumented-monolith-modernization/) Descoberta de domínio antes da migração O banco legado revela fronteiras; pessoas, autenticação e aeronaves saem gradualmente para contextos com persistência própria. [Ver o caso](/en/cases/undocumented-monolith-modernization/) Artifacts ## Selected projects Public repository ### [SMS Manager](/en/projects/sms-manager/) Importing CSV must not block the Nest API. The campaign persists and publishes to RabbitMQ; consumers in TypeScript, Go, or Rust record the result in Mongo. The API does not wait for the carrier. [Open the project](/en/projects/sms-manager/) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. Documented experiment ### [goc_mcp](/en/projects/goc-mcp/) Maestro (Codex/Cursor) delegates over MCP; FIFO daemon and OpenCode workers execute with state in SQLite. Orchestration worked, but measurement did not confirm cost or time reduction. [Open the project](/en/projects/goc-mcp/) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. Own product, desktop ### [md2cv](/en/projects/md2cv/) Profile and immutable versions stay in the machine's SQLite. The supervised agent only proposes changes under schema; ATS and PDF/DOCX export reuse the same graph, with no SaaS backend owning the data. [Open the project](/en/projects/md2cv/) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. [See all projects](/en/projects/) Path ## A trajectory of systems, not of job titles - 2025 to 2026 ### Architecture Rental and fleet platforms. The public proof from this period is the messaging redesign. Infosistemas - 2023 to 2025 ### Analytics on an external database SQL Server owned by another team. The dashboard had to work without relying on a schema we did not control. VBET - 2023 ### Post-sale over a legacy core PHP 5.7 still ran the operation. Automation had to free support without rewriting the monolith. Maxmilhas - 2022 to 2023 ### Multi-tenant marketplace White-label so banks could join without a per-client fork. South System / QUIQ-Itaú - 2021 to 2022 ### Incremental modernization The monolith had no original authors. Migration started with the parts of the database we could still read. Flapper Practice ## How the work proceeds Each step constrains the next. Without those constraints, architecture becomes personal taste. How the work proceeds - Context - Constraint - Decision - Evidence - Limit 01 ### Context Who is hurt when this breaks, and in which operation. 02 ### Constraint What cannot stop, change, or be treated as extra capacity. 03 ### Decision The path taken, with the discarded alternative kept visible. 04 ### Evidence What was measured, in which scenario, with whose authorship. 05 ### Limit The condition that would justify revisiting the decision. Contact ## Dealing with a system that's stopped being simple? Reach out directly via @imrafaeldev, no form — for professional conversation, start on LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/) [YouTube](https://www.youtube.com/@imrafaeldev) [GitHub](https://github.com/imrafaeldev) [LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Open the contact page](/en/contact/) --- # DiffVision URL: https://imrafaeldev.site/en/projects/diffvision > Local-first npm CLI for reviewing Git diffs, with local UI, in-repo comments, Markdown/JSON export, and MCP server. Visual AI review remains a mock. - [Home](/en/) - [Projects](/en/projects/) - DiffVision Public CLI; AI review still mock ## DiffVision The Git diff opens in the local UI; comments and Markdown export stay in the repository. Visual AI review remains a mock. [Repository](https://github.com/imrafaeldev/diffvision-app) Revisão local-first Diff no disco, UI local, comentários e export em `.diffvision/`. A plataforma remota fica de fora; a revisão por IA visual ainda é mock. - [01Problem](#problem) - [02Constraints](#constraints) - [03Decision](#decision) - [04Current state](#current-state) - [05Limitations](#limitations) ## Problem Reviewing a Git diff in a SaaS tool sends code away and mixes remote UI with local history. Review needs to work offline, with hunks, filters, bookmarks, and line-anchored comments. ## Constraints Preferences and reports must live in the repository itself (.diffvision/). The npm CLI starts local backend and UI. Assistant integration cannot be sold as ready while it is still a prototype. ## Decision CLI inspects Git, parses unified diff, and serves a web interface. Fastify backend with snapshot and WebSocket; React/Vite UI. Markdown/JSON export in the repository. diffvision-mcp package over stdio to summarize the repository, read patches, and record comments. The visual AI review assistant is declared mock/prototype; comment writing over MCP works. ## Current state Distributed as an npm CLI, running local-first. ## Limitations It does not replace the GitHub review flow. The visual AI flow must not be read as a finished product. --- # goc_mcp URL: https://imrafaeldev.site/en/projects/goc-mcp > Local agent orchestration over MCP in Go: maestro, FIFO daemon, OpenCode workers, and SQLite. Delivery worked, but the cost and time hypothesis was not confirmed in this measurement. - [Home](/en/) - [Projects](/en/projects/) - goc_mcp Documented experiment ## goc_mcp Maestro (Codex/Cursor) delegates over MCP; FIFO daemon and OpenCode workers execute with state in SQLite. Orchestration worked, but measurement did not confirm cost or time reduction. [Repository](https://github.com/imrafaeldev/goc_mcp) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. - [01Problem](#problem) - [02Constraints](#constraints) - [03Decision](#decision) - [04Current state](#current-state) - [05Limitations](#limitations) ## Problem A maestro (Codex or Cursor) needs to delegate work to OpenCode workers with an explicit lifecycle: plan, start, follow, respond, cancel, and recover, without infinite agent recursion. ## Constraints Everything is local. The daemon authenticates on loopback. There are global and per-workspace limits. Interrupted tasks must be reconciled. State corruption must not become blind writes. On the happy path: one OpenCode worker at a time, without automatic decomposition that leaves the maestro without control. ## Decision Go implementation: MCP gateways over stdio, single daemon, state machine, and FIFO executor. SQLite persistence with WAL; JSONL artifacts. OpenCode adapter with server as the primary path and CLI as fallback. Isolation against recursive delegation and read-only diagnostics if state corrupts. ## Current state Repository with tests, ADRs, and benchmarks. Orchestration worked. The measurements published in the project itself did not confirm the hypothesis of reducing time and cost. ## Limitations It is an experiment. It does not claim agent-engineering productivity gains in production. The negative result of the hypothesis is part of the artifact. --- # Projects URL: https://imrafaeldev.site/en/projects > Artifacts with problem, constraints, and current repository state. - [Home](/en/) - Projects ## Projects Artifacts with problem, constraints, and current repository state. Public repository ## [SMS Manager](/en/projects/sms-manager/) Importing CSV must not block the Nest API. The campaign persists and publishes to RabbitMQ; consumers in TypeScript, Go, or Rust record the result in Mongo. The API does not wait for the carrier. [Open the project](/en/projects/sms-manager/) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. Documented experiment ## [goc_mcp](/en/projects/goc-mcp/) Maestro (Codex/Cursor) delegates over MCP; FIFO daemon and OpenCode workers execute with state in SQLite. Orchestration worked, but measurement did not confirm cost or time reduction. [Open the project](/en/projects/goc-mcp/) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. Own product, desktop ## [md2cv](/en/projects/md2cv/) Profile and immutable versions stay in the machine's SQLite. The supervised agent only proposes changes under schema; ATS and PDF/DOCX export reuse the same graph, with no SaaS backend owning the data. [Open the project](/en/projects/md2cv/) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. Public CLI; AI review still mock ## [DiffVision](/en/projects/diffvision/) The Git diff opens in the local UI; comments and Markdown export stay in the repository. Visual AI review remains a mock. [Open the project](/en/projects/diffvision/) Revisão local-first Diff no disco, UI local, comentários e export em `.diffvision/`. A plataforma remota fica de fora; a revisão por IA visual ainda é mock. Editorial workstation ## [Post Engine](/en/projects/post-engine/) The adaptive interview extracts evidence; the hybrid gateway (LLM + heuristics) blocks invented lived experience. Only confirmed content moves to draft and Markdown or SlideMark export. [Open the project](/en/projects/post-engine/) Autoria antes da geração Entrevista extrai evidência; briefing e storyboard preparam o material; o gateway veta fabricado. Só o confirmado segue para rascunho e export. ## Other work, other contexts Visual projects to explore at your own pace. - [Gran Goiás Institutional site for Gran Goiás, a marble workshop with stone execution for large-scale works. Visit site](https://gran-goias.vercel.app/) - [Gabriel | Sports Nutrition Gabriel Pereira's institutional site for sports nutrition, with real content and strategy](https://gabriel-pereira-nutri.vercel.app/) --- # md2cv URL: https://imrafaeldev.site/en/projects/md2cv > Local-first desktop studio for professional profiles, Markdown resumes, immutable versions, ATS, and job matching with supervised agents. Data stays in the machine - [Home](/en/) - [Projects](/en/projects/) - md2cv Own product, desktop ## md2cv Profile and immutable versions stay in the machine's SQLite. The supervised agent only proposes changes under schema; ATS and PDF/DOCX export reuse the same graph, with no SaaS backend owning the data. [Repository](https://github.com/imrafaeldev/md2cv) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. - [01Problem](#problem) - [02Constraints](#constraints) - [03Decision](#decision) - [04Current state](#current-state) - [05Limitations](#limitations) ## Problem Professional profiles scatter across docs, LinkedIn, and exports. Each application asks for a different angle. Adapting a resume with an unbounded LLM invents experience and erases the context of the previous version. What is needed is a versioned graph on the machine that asks what is missing and rejects incomplete output, without promising hiring or automatic ATS approval. ## Constraints Desktop and local-first (Electron): no proprietary account and no backend owning the data. - Typed and validated IPC between renderer and main process. - SQLite with foreign keys, WAL, checksummed migrations, integrity checks, and backup before destructive operations. - Agents (Codex, Cursor, OpenCode) enter through isolated adapters, not as database owners. - Job matching never writes to the profile without explicit confirmation. - External access only on user action (company URL lookup or an AI CLI already configured on the computer); the product does not store provider credentials. ## Decision React/Vite renderer separated from the main process. The product organizes work in a single local graph: - Profile: experiences, education, courses, languages, projects, links, skills, and companies per person. - Resumes: Markdown, immutable versions, traceable restore, and evolution overview. - ATS: structural diagnostics and guiding scores; marked textual PDF and semantic DOCX. - Applications: company, job post, base resume, version, and state in the same context. - Agents: state machine for questions, attempts, and proposed new versions under schema; persists only on confirmation. - Data: versioned import and export of the full graph; Markdown compiler (unified/remark) shared by preview, audit, and export. Canonical flow: profile → base resume → immutable version → ATS audit → PDF/DOCX. The application branch goes through a supervised local agent before the adapted resume. ## Current state Public repository under MIT, documentation portal, and x64 releases for Linux (AppImage, .deb, .rpm) and Windows (NSIS and portable), with CI and Vitest and Playwright tests on persistence, agents, ATS, and Electron flows. The professional export used on this site comes from this product and is not read by the site at runtime. ## Limitations It does not replace human review nor promise hiring or automatic approval by recruiting platforms. Job matching depends on confirmed answers; the system refuses to fabricate experience. Windows binaries in this phase are not code-signed. The project is author-owned with no commercial purpose from the maintainer; the MIT license allows reuse under its terms. --- # Post Engine URL: https://imrafaeldev.site/en/projects/post-engine > Authorship-centered editorial workstation: interview, briefing, storyboard, and export after the gateway blocks fabricated content. Versioned prompts; isolated LLM workspace. - [Home](/en/) - [Projects](/en/projects/) - Post Engine Editorial workstation ## Post Engine The adaptive interview extracts evidence; the hybrid gateway (LLM + heuristics) blocks invented lived experience. Only confirmed content moves to draft and Markdown or SlideMark export. [Repository](https://github.com/imrafaeldev/post-engine) Autoria antes da geração Entrevista extrai evidência; briefing e storyboard preparam o material; o gateway veta fabricado. Só o confirmado segue para rascunho e export. - [01Problem](#problem) - [02Constraints](#constraints) - [03Decision](#decision) - [04Current state](#current-state) - [05Limitations](#limitations) ## Problem Generating a “professional” post with an LLM from a slogan invents biography. The flow must interview, identify gaps, prepare the briefing, draft, and export — and refuse what was not confirmed. ## Constraints Python core with boundaries between interview, generation, authorship preservation, segmentation, and persistence. Model calls in an isolated workspace, provider allowlist, and prompts as versioned contracts. Hybrid evaluation: LLM plus deterministic heuristics. Missing experience never becomes false lived experience. ## Decision Adaptive interviews, authorial briefing, storyboard, veto on fabricated content, SQLite prompt registry with rollback, textual interface, and React/Vite frontend to review phases. Markdown or SlideMark JSON export after evaluation. ## Current state Base with tests for interview, LLM isolation, registry, persistence, and SlideMark conversion. ## Limitations It is not a generic thought-leadership generator. Without real repertoire, the system refuses; it does not complete the biography. --- # SMS Manager URL: https://imrafaeldev.site/en/projects/sms-manager > Decoupled SMS campaigns: the Nest API persists and publishes, the queue delivers, and consumers in TypeScript, Go, or Rust record the result. Opaque token, Redis, and gRPC between services. - [Home](/en/) - [Projects](/en/projects/) - SMS Manager Public repository ## SMS Manager Importing CSV must not block the Nest API. The campaign persists and publishes to RabbitMQ; consumers in TypeScript, Go, or Rust record the result in Mongo. The API does not wait for the carrier. [Repository](https://github.com/imrafaeldev/sms-manager) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. - [01Problem](#problem) - [02Constraints](#constraints) - [03Decision](#decision) - [04Current state](#current-state) - [05Limitations](#limitations) ## Problem SMS campaigns start from CSV files, users, companies, and authentication between services. Validating and persisting in the same process that fires thousands of messages couples the API to the pace of the carrier and the queue. ## Constraints The environment must be reproducible. Authentication between services cannot depend on JWT without revocation. Consumers in more than one language exist to compare the same messaging contract. ## Decision Seven applications: NestJS user/auth and company/campaign APIs; equivalent consumers in Node.js/TypeScript, Go, and Rust; declarative provisioner for exchanges, queues, and bindings; CSV bulk generator. PostgreSQL/TypeORM for relational API data; MongoDB for consumption results; Redis for opaque revocable token cache; gRPC for authentication between services; RabbitMQ with topic exchanges for batches. The API publishes and moves on without blocking on the carrier pace. ## Current state Public repository with Docker Compose for PostgreSQL, MongoDB, Redis, and RabbitMQ. The architecture separates domain, application, and infrastructure in the users, authentication, and companies contexts; the three consumers implement the same message contract. ## Limitations It is a study artifact and local messaging operation, not a commercial product with a carrier SLA. The three consumers demonstrate the contract; they do not claim the three languages run together in the author’s production. --- # Resume URL: https://imrafaeldev.site/en/resume > Rafael Pereira - [Home](/en/) - Resume ## Engineering for systems that stopped being simple Senior Backend Engineer Senior Backend Engineer with more than seven years of experience in distributed systems, transactional platforms, and legacy modernization. I combine hands-on delivery with architecture, technical leadership, mentoring, and collaboration with product and stakeholders. My primary axis is Node.js, Go, and Java; React and Angular are complementary work across products that need continuity between backend and frontend. ## Experience - Feb 2025 to May 2026 Remote Infosistemas Senior Software Engineer / Software Architect Expand experience Collapse experience Led NestJS and Go integrations and redesigned flows between microservices, reducing intermittent failures in critical flows by approximately 98%. Context Worked on management platforms for rental companies, fleets, and automakers, within the architecture team and alongside operations and data specialists. Contribution Led NestJS and Go integrations and redesigned flows between microservices, making failure behavior more predictable in critical flows. [Read the full experience](/en/experience/infosistemas/) - Jul 2025 to Dec 2025 Remote EDS (Civil Police of Rio de Janeiro) Backend Engineer — Consulting Expand experience Collapse experience Structured a health management system with NestJS for a critical public operation and evolved backend routes with a focus on security, access, and traceability. Context The consulting work covered sensitive public systems, including a high-volume health management system and an evolving legal ERP. Contribution Structured the NestJS backend, refactored legacy routes, and strengthened security, access control, and traceability for sensitive flows. [Read the full experience](/en/experience/eds-policia-civil-rio/) - Mar 2025 to Jun 2025 Remote Azify Senior Backend Engineer — Consulting Expand experience Collapse experience Built a settlement engine with NestJS and reduced critical financial API latency by 30% through profiling and optimization. Context Worked in fintech and crypto assets, on financial services where consistency, security, and stability had direct operational impact. Contribution Built a NestJS settlement engine with financial integrations and reduced critical API latency by 30% through profiling and optimization. [Read the full experience](/en/experience/azify/) - Oct 2023 to Feb 2025 Remote VBET Senior Backend Engineer Expand experience Collapse experience Used Go, goroutines, and channels to reduce the first commission-calculation step from about seven to three minutes, while also contributing to the React application. Context The analytics product served iGaming influencers and affiliates; its dashboard combined commission metrics and allowed a small lag for same-day data, while payouts required consolidated data. Contribution Restructured the API and calculation path with Go, goroutines, and channels, introduced ETL, pre-computation, and caching, and collaborated on the React application. The measured line went from about seven minutes at peak to about three minutes, about one minute, approximately 15 seconds without cache, and under one second with a warm cache, each belonging to its stage. [Read the full experience](/en/experience/vbet/) - Apr 2023 to Oct 2023 Remote Maxmilhas Full Stack Software Engineer Expand experience Collapse experience Built Node.js and NestJS microservices for cancellation and rebooking automation, reducing manual support intervention by 34%. Context Worked on travel post-sale flows, including cancellations, rebooking, coupons, and customer communication. Contribution Built Node.js and NestJS microservices and automated post-sale rules, calculations, and validations; the change reduced the need for manual support intervention by 34%. [Read the full experience](/en/experience/maxmilhas/) - Jun 2022 to Apr 2023 Remote South System (assigned to QUIQ/Itaú) Backend Engineer Expand experience Collapse experience Architected a multi-tenant marketplace with Node.js and asynchronous services in Go, structuring critical tests with approximately 95% coverage. Context The work focused on a white-label, multi-tenant marketplace for financial institutions, prepared to incorporate new banks without client-specific forks. Contribution Participated in architecture and rule refinement, using Node.js and asynchronous Go services to isolate tenant variations; structured critical tests with approximately 95% coverage. [Read the full experience](/en/experience/south-system-quiq-itau/) - Sep 2021 to Jun 2022 Remote Flapper Full Stack Software Engineer Expand experience Collapse experience Migrated people, authentication, and aircraft modules to Node.js, NestJS, and Go services; domain separation reduced the table count by approximately 25%. Context The executive aviation platform depended on a monolith over seven years old, with little documentation and rules that were difficult to separate without interrupting operations. Contribution Mapped domain boundaries and led an incremental modernization, migrating modules to Node.js, NestJS, and Go services; the separation reduced the table count by approximately 25%. [Read the full experience](/en/experience/flapper/) - Jan 2021 to Aug 2021 Remote Sustentec Mid-level Full Stack Software Engineer Expand experience Collapse experience Maintained and evolved a laboratory management system with Java, Spring Boot, and Angular, adding integration tests where none existed before. Context Worked on laboratory, research, and development systems, combining maintenance of an existing product with functional evolution. Contribution Maintained and evolved the system with Java, Spring Boot, and Angular, added integration tests, and delivered reports and end-to-end features. [Read the full experience](/en/experience/sustentec/) - Nov 2019 to Jan 2021 Campina Grande, Paraíba / Remote Braistech Mid-level Full Stack Software Engineer Expand experience Collapse experience Led the structure of the main system with Node.js and NestJS and designed microservices for the business core. Context In a product involving crypto-asset contracts and financial movements, took broad responsibility within a small team. Contribution Led the structure of the main system with Node.js and NestJS, designed microservices for the business core, helped evolve the code toward Clean Architecture, and mentored junior developers. [Read the full experience](/en/experience/braistech/) ## Specialties - ### Node.js Specialized in APIs, microservices, and integrations with NestJS. - ### Go Advanced experience with APIs, concurrency, and high-volume processing. - ### Java Experience maintaining and evolving systems with Spring Boot. - ### React Worked on the evolution and maintenance of React applications. - ### Angular Worked on an Angular web application with forms, validation, and API consumption. ## Education - ### Systems Analysis and Development Technology degree · UNOPAR — Universidade Norte do Paraná completed in 2024 - ### Cloud Computing Graduate degree · Anhanguera Educacional completed in 2025 ## Public links - [LinkedIn — Open public link](https://www.linkedin.com/in/this-rafael-pereira/) - [GitHub — Open public link](https://github.com/this-rafael) --- # La mayoría de los problemas de performance de backend empieza cerca de los datos URL: https://imrafaeldev.site/es/articulos/backend-performance-cerca-de-los-datos > Antes de subir máquina, caché o cola, mide el trabajo de la petición. Plan de ejecución, N+1, CAST en columna y loops secuenciales. - [Inicio](/es/) - [Artículos](/es/articulos/) - La mayoría de los problemas de performance de backend empieza cerca de los datos ## La mayoría de los problemas de performance de backend empieza cerca de los datos API lenta y la reunión ya se llena de soluciones. Yo empiezo cerca de los datos — no por culpar a la base, sino porque esa verificación suele dar señal rápida. 16 de julio de 2026 - [Rendimiento](/es/articulos/?topic=performance) - [Arquitectura](/es/articulos/?topic=arquitetura) Una API se vuelve lenta y la reunión ya se llena de soluciones antes de ganar una medición. Aparecen más CPU y RAM, caché, cola, microservicios y refactorización. A veces alguien propone hasta cambiar el lenguaje. Raramente la primera sugerencia es abrir el plan de ejecución. Por eso, empiezo la investigación cerca de los datos. No porque la base sea la culpable por defecto, sino porque mucho pasa por ahí y esa verificación suele dar señal rápida. Si la consulta está saludable, saco la base del frente y sigo el flujo de la petición. ## Soluciones rápidas también esconden trabajo caro Caché y cola resuelven problemas reales. Más máquina también puede ser la decisión correcta. Cuando entran por reflejo, sin embargo, esos recursos pueden solo cambiar el tamaño de la cuenta mientras nadie sabe qué tramo de la petición está reteniendo la respuesta. Subir CPU reduce la disputa por recurso, la caché saca algunas peticiones del camino y la cola absorbe un pico. Mientras tanto, una consulta que hace un trabajo enorme para devolver casi nada sigue cara en cada ejecución. Lo mismo vale para un loop que dispara una llamada por ítem. La latencia puede caer por un tiempo, la alerta deja de sonar y el equipo respira. Cuando la carga crece, la operación cara reaparece, ahora acompañada de una infraestructura mayor. La pregunta que debe venir antes de la discusión de arquitectura: ¿cuánto trabajo está produciendo esta petición para entregar el resultado? ## Casi duplicamos la máquina y el dashboard siguió lento Fue lo que pasó en un dashboard en el que trabajé. Cargaba de forma síncrona, y una única consulta hacía todo a la vez: buscaba los datos, cruzaba varias informaciones y calculaba los valores exhibidos. Como la CPU de la base quedaba muy alta, casi duplicamos CPU y RAM. La base quedó más grande. El dashboard siguió pasando del minuto para cargar. La misma consulta aún concentraba todo el trabajo pesado en una única ejecución. Fue cuando abrimos el plan de ejecución. La pantalla devolvía pocos indicadores, pero la consulta atravesaba relaciones, formaba un volumen intermedio grande y gastaba CPU en agregaciones antes de llegar a ellos. La respuesta final era pequeña. El trabajo para producirla, enorme. Duplicar CPU y RAM había dado más aire a la base, pero la búsqueda seguía obligándola a hacer todo a la vez. El plan mostró que la investigación necesitaba entrar en el camino recorrido por la consulta. ## Lo que necesito ver antes de tocar la base Para que la base se vuelva sospechosa de verdad, quiero el plan de ejecución y las métricas apuntando en esa dirección. Tiempo de la consulta, lecturas y cardinalidad suelen confirmar o eliminar una hipótesis en pocos minutos. Si esas medidas están saludables, saco la base del frente. Ya tomé API lenta con la consulta respondiendo dentro de lo esperado, mientras el retraso estaba en el procesamiento de la aplicación y en llamadas remotas hechas de forma secuencial. A partir de ahí, seguir buscando un defecto en la base sería solo insistir en la capa equivocada. Antes de profundizar, paso un radar corto por la consulta y por el código. Una query para cargar la lista seguida de otra para cada registro pide un conteo de consultas. Ese N+1 aparece con frecuencia cuando el ORM deja las relaciones para que el backend las busque una a una. Un JOIN que multiplica filas antes de la agregación pide la medición del volumen intermedio. Índice ausente pide plan. Un filtro que aplica una función sobre la columna indexada también. En el código, busco llamadas remotas o consultas con await dentro de for. Cada espera puede parecer pequeña aisladamente y aun así dominar el tiempo total cuando todas ejecutan en fila. Ninguna de estas señales cierra el diagnóstico. Un N+1 en una ruta con dos ítems puede tener impacto irrelevante, un índice nuevo puede ayudar poco en una columna con baja selectividad y un join voluminoso quizás sea necesario para producir el resultado. Por eso, cuento las consultas, cronometro el loop entero y reviso en el plan cuántas filas y lecturas se produjeron. Ese radar solo elige la primera medición. La próxima capa de la investigación viene de la cuenta. ## Una línea correcta puede desperdiciar el índice Una de esas líneas suele pasar desapercibida en la revisión. Devuelve los registros de un día entero. WHERE CAST(campo_data_hora AS date) = @data El resultado de la pantalla parece correcto. En el plan, la historia puede ser otra. Aplicar CAST a la columna obliga a la consulta a transformar los valores antes de la comparación. Con un índice en campo_data_hora, esto puede impedir una búsqueda directa por el intervalo, aumentar bastante las lecturas e incluso llevar a un scan. Cuando el pedido es por los registros de un día entero, calculo los bordes fuera de la columna. WHERE campo_data_hora >= @inicio_do_dia AND campo_data_hora < @inicio_do_proximo_dia Si el inicio es el 16 de julio a la medianoche, el límite siguiente es el 17 de julio a la medianoche. Así entran todos los valores del día 16, incluso aquellos con fracciones de segundo al final, sin depender de 23:59:59.999. El resultado permanece correcto, pero ahora existe un intervalo que el índice puede recorrer. La confirmación viene de la comparación de las lecturas y del operador de acceso en los dos planes. Si la métrica no cambia, la hipótesis no se sostuvo. ## Lee el plan por la secuencia del trabajo Después de comparar las versiones del filtro, leo el plan como una historia del trabajo producido por la consulta. Empiezo por el tiempo total y por las lecturas. Si la pantalla devuelve diez indicadores, pero la ejecución hace cientos de miles de lecturas, hay una cuenta por explicar. Después comparo la cardinalidad estimada con la real. Cuando el optimizador esperaba pocas filas y recibió muchas, puede haber elegido joins, memoria y agregaciones para un escenario bien menor que el encontrado durante la ejecución. Enseguida acompaño dónde crecen los datos. Busco el operador en que un JOIN lleva miles de filas a cientos de miles, poco antes de que un filtro o GROUP BY lo reduzca todo de nuevo. La respuesta final pequeña puede esconder un volumen intermedio enorme. Es en ese camino donde agregaciones y ordenaciones empiezan a gastar demasiada CPU. Tampoco tomo el porcentaje de costo exhibido por el plan como sentencia. Sirve para elegir dónde medir. Marco el operador caro, cuántas filas entran y salen de él y cuánto tiempo consume. Si el costo aparece después de la multiplicación de las filas y antes de los pocos indicadores necesarios, ya existe una hipótesis concreta para probar. ## Romper la consulta donde crece el costo Lo que resolvió el dashboard fue romper la consulta y aprovechar los índices correctamente. La división no ocurrió de forma arbitraria. El propio plan mostró dónde el costo empezaba a crecer. Mantuvimos en la base los filtros y la búsqueda indexada de los registros necesarios. Llevar ese recorte a la aplicación haría que el servidor recibiera un conjunto mayor antes de poder descartarlo. También desperdiciaría el camino que los índices ya acortaban. La composición final de los indicadores fue a la aplicación. Esa etapa venía después de las relaciones y agregaciones que elevaban el volumen intermedio y mantenían alta la CPU de la base. Con los datos ya recortados, el servidor podía montar los valores de la pantalla sin concentrar toda la ejecución en una única query. La base pasó a reducir el conjunto --- # Desenoxidando la lógica #02: Container With Most Water URL: https://imrafaeldev.site/es/articulos/desenoxidando-logica-container-with-most-water > LeetCode 11 en JavaScript: un intento basado en los vecinos, la revisión de la hipótesis y la solución de dos punteros. - [Inicio](/es/) - [Artículos](/es/articulos/) - Desenoxidando la lógica #02: Container With Most Water ## Desenoxidando la lógica #02: Container With Most Water Intenté elegir el próximo paso mirando solo a los vecinos. Funcionó en algunos casos, pero el problema pedía una visión más amplia. 15 de septiembre de 2026 - [Rendimiento](/es/articulos/?topic=performance) - [Trade-offs](/es/articulos/?topic=trade-offs) Después de resolver el primer ejercicio de la serie, seguí en LeetCode para trabajar la lógica sin pedir una solución lista. El desafío 011 es [Container With Most Water](https://leetcode.com/problems/container-with-most-water/). Recibimos un array de alturas y necesitamos elegir dos líneas que formen el recipiente con el área mayor. ## Cómo calcular el área Si elijo las posiciones left y right, el ancho es la distancia entre ellas. La altura del recipiente está limitada por la menor de las dos líneas. área = min(altura izquierda, altura derecha) × distancia Por ejemplo, con estas alturas: [1, 8, 6, 2, 5, 4, 8, 3, 7] Las líneas en las posiciones 1 y 8 tienen alturas 8 y 7. La menor altura es 7, y la distancia entre ellas es 7. Esa combinación produce un área de 49. ## Mi primer intento Empecé con dos punteros, uno en cada punta del array. Después de calcular el área actual, simulaba dos posibilidades: - avanzar el puntero de la izquierda; - retroceder el puntero de la derecha. Calculaba el área de los dos próximos pares y elegía la mayor. El tramo principal era este: const paddingLeftArea = Math.min(heights[leftIndex + 1], heights[rigthIndex]) * (rigthIndex - leftIndex + 1); const paddingRightArea = Math.min(heights[leftIndex], heights[rigthIndex - 1]) * (rigthIndex - 1 - leftIndex); if (paddingLeftArea > paddingRightArea && paddingLeftArea > maxArea) { leftIndex += 1; } else { rigthIndex -= 1; } El problema estaba en la hipótesis. La mejor decisión local no garantiza la mejor área en el resto del array. Intentaba adivinar el camino mirando solo los dos próximos movimientos. También había un error en la fórmula de la distancia de ese borrador: para un par de posiciones, el ancho es right - left. ## La observación que destraba el problema El área depende de dos cosas: ancho y menor altura. Cuando los punteros están en las posiciones left y right, mover el puntero de la mayor altura no puede aumentar la altura mínima del recipiente. El ancho siempre disminuye, y la altura que limita el área sigue presente. Por eso, el puntero que debe avanzar es el de la menor altura. Es el único movimiento que puede encontrar una línea más alta y compensar la pérdida de ancho. Si las alturas son iguales, cualquiera de los dos puede avanzar. En el código, elegí avanzar el de la izquierda cuando heights[leftIndex] <= heights[rigthIndex]. ## Solución con dos punteros /** * @param {number[]} heights * @return {number} */ var maxArea = function (heights) { let leftIndex = 0; let rigthIndex = heights.length - 1; let maxArea = 0; while (leftIndex < rigthIndex) { const minH = Math.min(heights[leftIndex], heights[rigthIndex]); const currentArea = minH * (rigthIndex - leftIndex); maxArea = Math.max(maxArea, currentArea); if (heights[leftIndex] <= heights[rigthIndex]) { leftIndex++; } else { rigthIndex--; } } return maxArea; }; En cada ronda, calculo el área del par actual, actualizo la mayor área encontrada y muevo uno de los punteros. El while se aproxima al centro y termina. El detalle del avance importa. En la versión que yo había escrito, los punteros solo avanzaban cuando el área actual no era mayor que maxArea. Si se encontraba una nueva área máxima, la misma combinación se calculaba de nuevo, sin salir del loop. La corrección fue separar las dos decisiones: registrar el área y, después, mover el puntero de la menor altura. ## El resultado de los intentos El historial de LeetCode quedó así: - JavaScript: aceptada, 3 ms y 63.6 MB. - JavaScript: respuesta incorrecta. - JavaScript: respuesta incorrecta. - Go: aceptada, 0 ms y 9.6 MB. - TypeScript: aceptada, 3 ms y 63.9 MB. - Go: respuesta incorrecta. Fueron tres intentos con respuesta incorrecta antes de llegar a las soluciones aceptadas en JavaScript, Go y TypeScript. Más que contar envíos, yo quería mirar el error, entender la hipótesis que falló e intentarlo de nuevo sin tercerizar todo el razonamiento. ## Complejidad El algoritmo recorre el array una vez. En cada iteración, uno de los punteros avanza, entonces la complejidad de tiempo es O(n) y la complejidad de espacio es O(1). El primer intento también usaba dos punteros, pero hacía trabajo extra para comparar posibilidades futuras. La segunda solución usa una propiedad del problema para descartar con seguridad parte de las combinaciones. Ese fue el ejercicio esta vez: no confundir una elección que parece buena ahora con una decisión que el problema realmente permite justificar. --- *Serie Desenoxidando la lógica #02 — Container With Most Water. Problema en [leetcode.com/problems/container-with-most-water](https://leetcode.com/problems/container-with-most-water/).* Geometria: min da altura × largura Nas posições 1 e 8, a menor altura é 7 e a largura é 7. O recipiente produz área 49. Dois ponteiros: mova a menor linha A largura sempre diminui. Só mover a menor altura pode encontrar um limite maior; mover a maior mantém o gargalo e pode ser descartado. --- # Desenoxidando la lógica #01: Group Anagrams URL: https://imrafaeldev.site/es/articulos/desenoxidando-logica-group-anagrams > LeetCode 49 en Go: de la clave por sort al conteo de 26 letras, y el hábito de seguir pensando después de que el código funciona. - [Inicio](/es/) - [Artículos](/es/articulos/) - Desenoxidando la lógica #01: Group Anagrams ## Desenoxidando la lógica #01: Group Anagrams Yo no había dejado de escribir código. Lo que cambió fue tercerizar partes del razonamiento. Group Anagrams fue el ejercicio para recuperar el hábito. 17 de agosto de 2026 - [Trade-offs](/es/articulos/?topic=trade-offs) - [Rendimiento](/es/articulos/?topic=performance) Después de años programando, percibí que mi lógica estaba oxidada. Yo no había dejado de escribir código. Lo que cambió fue que, poco a poco, empecé a tercerizar partes del razonamiento que antes necesitaba ejercitar solo. Elegí el ejercicio [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/) en LeetCode. La propuesta es recibir una lista de strings y reunir los anagramas en el mismo grupo. ## Lo que necesitamos resolver Entrada: ["eat", "tea", "tan", "ate", "nat", "bat"] Resultado posible: ["eat", "tea", "ate"] ["tan", "nat"] ["bat"] El orden de los grupos no importa. ## ¿Qué es un anagrama? Toma eat, tea y ate. Cada una tiene a una vez, e una vez y t una vez. La posición cambia; la cantidad de cada letra sigue igual. El algoritmo necesita transformar esas palabras en una representación común. Si las tres producen la misma clave, puedo usar esa clave en un map y colocarlas en el mismo grupo. El primer problema es crear esa clave. ## Mi primera respuesta fue ordenar Empecé usando el string ordenado como clave: eat → aet tea → aet ate → aet Las tres producen aet. ### Solución usando sort Esta fue la primera solución que se me ocurrió. No intentaba la implementación más concisa de inmediato. Quería montar una solución coherente y entender dónde podría mejorar. func sortString(str string) string { b := []byte(str) slices.Sort(b) return string(b) } func groupAnagrams(strs []string) [][]string { mapping := make(map[string][]string) for _, str := range strs { sortedStr := sortString(str) mapping[sortedStr] = append(mapping[sortedStr], str) } result := make([][]string, 0, len(mapping)) for _, group := range mapping { result = append(result, group) } return result } Lo que ocurre en ese código: - sortString(str) transforma el string en bytes, ordena los caracteres y devuelve un nuevo string. - sortedStr := sortString(str) produce la clave de aquel término. - mapping[sortedStr] = append(...) usa esa clave para acumular los anagramas en el mismo grupo. - Para eat, tea y ate, sortedStr será siempre aet. El mapa empieza a quedar así: "aet" -> ["eat", "tea", "ate"] "ant" -> ["tan", "nat"] "abt" -> ["bat"] Calculo una clave y añado la palabra directamente al grupo correspondiente. El map evita comparar cada string con todas las demás. ## ¿Dónde está el costo de ese enfoque? Para descubrir la clave, necesito ordenar cada string. Si un string tiene k caracteres, esa ordenación cuesta aproximadamente O(k log k). Repitiendo para n strings, la parte dominante queda en O(n × k log k). ### ¿Por qué quitar el sort? Para eat, ordenaba los caracteres para llegar a aet. Para tea, hacía otra ordenación para llegar al mismo aet. El sort funciona porque crea una representación común. Solo que el problema no exige ordenar nada. Para saber si dos strings son anagramas, basta verificar si poseen la misma cantidad de cada letra. ## Esa observación cambia la solución En lugar de colocar las letras en el mismo orden, puedo contar cuántas veces aparece cada una. En este ejercicio, las entradas usan letras minúsculas de a a z. Cada string puede representarse por 26 contadores. tea y ate producen el mismo conteo. No necesito reorganizar ningún carácter. Solo recorro el string y cuento las ocurrencias. Para eat, la parte relevante queda: a = 1 e = 1 t = 1 ### Solución usando conteo var key [26]uint8 crea 26 posiciones, una para cada letra de a a z. key[str[i]-'a']++ encuentra la posición de cada carácter e incrementa el contador. Después, groups[key] = append(groups[key], str) usa el propio vector de frecuencias como clave del grupo. func groupAnagrams(strs []string) [][]string { groups := make(map[[26]uint8][]string, len(strs)) for _, str := range strs { var key [26]uint8 for i := 0; i < len(str); i++ { key[str[i]-'a']++ } groups[key] = append(groups[key], str) } result := make([][]string, 0, len(groups)) for _, group := range groups { result = append(result, group) } return result } ## ¿Qué cambió en complejidad? - Ordenación: O(k log k) por string y O(n × k log k) para n strings. - Conteo: O(k) por string y O(n × k) para n strings. La mejora apareció cuando percibí que la ordenación hacía un trabajo que el problema no exigía. El cambio principal es de O(k log k) a O(k) por string. ## La primera solución no estaba equivocada Resuelve el problema y, según el contexto, podría bastar. El hábito que quería recuperar era seguir pensando después de que el código empieza a funcionar. Encontrar una solución, volver al problema y preguntar: ¿qué trabajo está haciendo mi algoritmo sin necesitarlo? No hago estos ejercicios porque LeetCode represente todo el trabajo de ingeniería de software, ni para disputar la solución más sofisticada. Los hago porque percibí que usar IA todos los días redujo la cantidad de veces en que necesito insistir solo en un problema. Quiero reservar espacio para ejercitarlo de nuevo: leer, intentar, errar, revisar el enfoque y solo después comparar caminos. --- *Serie Desenoxidando la lógica #01 — Group Anagrams. Adaptado del carrusel autoral; problema en [leetcode.com/problems/group-anagrams](https://leetcode.com/problems/group-anagrams/).* Chave por sort e mapa Cada string vira chave ordenada (eat → aet). O mapa agrupa anagramas sob a mesma chave sem comparar cada par. Contagem [26]uint8 vs sort Vetor de frequências a–z vira chave. Contagem custa O(k) por string; sort custava O(k log k). Total: de O(n × k log k) para O(n × k). --- # Deja de ser rehén de las dependencias. Saluda al patrón de diseño Adapter URL: https://imrafaeldev.site/es/articulos/design-patterns-adapter > Con el Adapter, el servicio depende de un protocolo y los adapters traducen MySQL, PostgreSQL o mocks — sin acoplar la regla de negocio al driver. - [Inicio](/es/) - [Artículos](/es/articulos/) - Deja de ser rehén de las dependencias. Saluda al patrón de diseño Adapter ## Deja de ser rehén de las dependencias. Saluda al patrón de diseño Adapter Migrar de MySQL a PostgreSQL no exige reescribir el servicio. El Adapter aísla el plugin tras un contrato que la regla de negocio entiende. 26 de abril de 2022 - [Arquitectura](/es/articulos/?topic=arquitetura) Con el patrón Adapter, aislamos la regla de negocio de la dependencia concreta. ## Escenario inicial Tenemos un backend con CRUD simple de usuario: crear, editar, recuperar y eliminar por endpoints en la API. Los datos van a una base cualquiera — digamos MySQL — y la estructura nace como **Controller → Service → Database**. Hasta aquí, el equipo está cómodo. Hasta que alguien decide: la semana que viene migramos de MySQL a PostgreSQL. A partir de ahí la casa se cae para tecnología. Además de reestructurar la base, el equipo necesita cazar referencias a MySQL: inserciones, conexión, queries esparcidas. La mayoría de las veces esto retrasa la entrega, reduce la calidad, salta pruebas e introduce bugs. Sería mejor disminuir la dependencia entre la regla de negocio y quien ejecuta la operación específica — en este caso, la base. El Adapter ayuda a construir el sistema así. ## Patrón Adapter Tenemos un plugin, library, module o servicio de terceros que hace algo que queremos en la regla de negocio — aquí, persistir datos. El camino: - Definir una interfaz con el contrato de lo que necesitamos. - Exponer solo métodos que tengan sentido en el contexto (SOLID). - Implementar la interfaz en clases que adaptan el código de terceros. Creamos CreateDatabaseCustomerProtocol con un método create que recibe CustomerInputEntity y retorna SuccessfulEntityCreation: interface CustomerInputEntity { name: string; email: string; birthDate: Date; } interface SuccessfulEntityCreation { readonly id: number; readonly name: string; readonly email: string; readonly birthDate: Date; } interface CreateDatabaseCustomerProtocol { createCustomerOnDatabase( customer: CustomerInputEntity, ): SuccessfulEntityCreation; } En lugar de **Controller → Service → Database**, pasamos a **Controller → Service → Protocols → Plugin**. El servicio pierde el conocimiento de cómo el CRUD llega a la base. Está compuesto por protocolos; la implementación concreta entra en tiempo de ejecución — por inyección de dependencias. Mientras usamos PostgreSQL, implementamos los protocolos en los adapters (o connectors). CreateDatabaseCustomerProtocol puede ser implementado por CreateDatabaseCustomerPostgresqlAdapter, CreateDatabaseCustomerMysqlAdapter, CreateDatabaseCustomerMongoDBAdapter o CreateDatabaseCustomerMockedAdapter. El servicio queda así: class CustomerService { constructor( private readonly createCustomer: CreateDatabaseCustomerProtocol, ) {} public register(customer: CustomerInputEntity): SuccessfulEntityCreation { return this.createCustomer.createCustomerOnDatabase(customer); } } Para el servicio, da igual si la base devuelve JSON, XML u otro formato — el adapter traduce al contrato que la regla de negocio espera. ## Ventajas - **Mantenimiento:** cualquier plugin puede sustituirse sin reescribir el servicio. - **Pruebas:** para probar solo la regla de negocio, inyecta un adapter mock que implemente el mismo protocolo. - **Código limpio:** responsabilidades separadas; la regla de negocio no carga detalles del driver. ## Próximo paso Toma un frontend con decenas de bibliotecas e identifica lo que realmente usas. Elige una funcionalidad — convertir reales a dólares, por ejemplo. Describe el contrato (entrada y salida) e implementa un adapter sobre la biblioteca que hoy lo hace. Repite donde la dependencia moleste. ## Relación con otros patrones En el artículo [Design Patterns: Strategy](/es/articulos/design-patterns-strategy/), el foco es intercambiar algoritmos tras un contrato. El Adapter aísla dependencias externas tras una interfaz propia. Ambos se complementan: Strategy varía comportamiento; Adapter traduce el mundo exterior. --- *Publicado originalmente en [LinkedIn](https://www.linkedin.com/pulse/pare-de-ser-ref%C3%A9m-das-depend%C3%AAncias-diga-bem-vindo-ao-design-rafael/) el 26 de abril de 2022.* Adapter: protocolo e implementações CustomerService usa CreateDatabaseCustomerProtocol. MysqlAdapter e PostgresqlAdapter implementam o contrato e traduzem para cada banco. Camadas: antes e depois Acoplamento direto ao MySQL dificulta migração. Com protocolo e adapter, troca-se o plugin sem reescrever o serviço. --- # Design Patterns: Strategy URL: https://imrafaeldev.site/es/articulos/design-patterns-strategy > Cómo el patrón Strategy encapsula algoritmos intercambiables y evita frágiles cadenas de if/else, con un ejemplo de calculadora en TypeScript. - [Inicio](/es/) - [Artículos](/es/articulos/) - Design Patterns: Strategy ## Design Patterns: Strategy Las cadenas de if/else crecen y se vuelven frágiles. Strategy aísla cada algoritmo tras un contrato. 5 de mayo de 2022 - [Arquitectura](/es/articulos/?topic=arquitetura) Las cadenas de if/else crecen y se vuelven frágiles. El patrón Strategy encapsula cada algoritmo en su propia clase y permite intercambiar implementaciones en tiempo de ejecución sin alterar el código que las consume. ## El problema: if/else infinito Una calculadora con suma, resta, multiplicación y división suele nacer como una clase DefaultCalculator: métodos privados por operación y una función pública que elige cuál invocar con switch o cadena de if/else. class DefaultCalculator { public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; switch (operator) { case "*": return firstOperand * secondOperand; case "+": return firstOperand + secondOperand; case "-": return firstOperand - secondOperand; case "/": return firstOperand / secondOperand; case "**": return firstOperand ** secondOperand; case "%": return firstOperand % secondOperand; default: throw new Error("Operator not found!"); } } } El problema aparece cuando la calculadora necesita cubrir más operaciones binarias entre enteros: porcentaje, exponenciación, módulo, shift de bits. Cada funcionalidad nueva altera la implementación original, sube el acoplamiento y encarece el mantenimiento. ## ¿Qué es el patrón Strategy? El patrón define la funcionalidad por medio de un contrato (interfaz), implementado según el contexto. La interfaz define la operación; las implementaciones concretas definen su ejecución. El código consumidor depende de la abstracción. Cada estrategia queda aislada en su propia clase. Nuevos comportamientos entran sin alterar el código existente, alineado al Open/Closed Principle. ## Definiendo el contrato El primer paso es la interfaz del contrato de la estrategia. En la calculadora, algo que reciba dos números y devuelva el resultado: interface BinaryOperationParameters { firstOperand: number; secondOperand: number; operator: string; } type Result = number; interface BinaryOperationStrategy { calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result; } ## Implementando estrategias concretas Cada operación matemática se vuelve una clase que implementa BinaryOperationStrategy y ejecuta una única operación. Suma y división: class Sum implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; return firstOperand + secondOperand; } } class Division implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; if (secondOperand === 0) { throw new Error("Division by zero is not allowed!"); } return firstOperand / secondOperand; } } ## El Context y la Factory Para amarrar las estrategias, entran un Context y una Factory (o Analyzer). En ContextAnalyzer, un método evalúa el operador y retorna la Strategy correcta: class ContextAnalyzer { public getInstance(operator: string): BinaryOperationStrategy { switch (operator) { case "*": return new Multiplication(); case "+": return new Sum(); case "-": return new Subtraction(); case "/": return new Division(); case "%": return new Percent(); case "**": return new Pow(); default: throw new Error("Operator not found!"); } } } El contexto recibe y ejecuta la estrategia. Conoce solo el contrato, no la implementación concreta. La antigua DefaultCalculator pasa a recibir ese ContextAnalyzer por inyección: class Calculator { constructor(private readonly contextAnalyzer: ContextAnalyzer) {} /** * La implementación de calculate en Calculator no cambia por operación. * Lo que crece es el ContextAnalyzer, que añade un case por operación nueva. */ public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; return this.contextAnalyzer .getInstance(operator) .calculate({ firstOperand, secondOperand }); } } ## ¿Por qué usar Strategy? Strategy ayuda en legado con varias reglas de negocio, cada una representada por un if y una implementación extensa. La parte común queda en el contrato; cada variación de regla queda en su propia clase; un analizador de contexto (resolver/factory) elige la estrategia. En cada petición, el código evalúa el contexto de la operación y selecciona la implementación correspondiente al contrato. ## Relación con otros patrones - Adapter: Strategy varía comportamiento; Adapter aísla dependencias externas tras una interfaz propia. - SOLID (OCP): Strategy es una forma de aplicar el Open/Closed Principle. Strategy: contrato, seleção e concretas Calculator depende do contrato BinaryOperationStrategy. ContextAnalyzer escolhe a concreta (ex.: Sum) pelo operador; Sum, Division e Pow implementam o mesmo contrato. --- # Intensivo de Golang: concurrencia, resiliencia y sistemas distribuidos en 30 minutos URL: https://imrafaeldev.site/es/articulos/go-intensivo > Revisión práctica de Go para backend: goroutines, context, backpressure, idempotencia, Kubernetes, observabilidad y rendimiento. - [Inicio](/es/) - [Artículos](/es/articulos/) - Intensivo de Golang: concurrencia, resiliencia y sistemas distribuidos en 30 minutos ## Intensivo de Golang: concurrencia, resiliencia y sistemas distribuidos en 30 minutos Una guía de 30 minutos para reactivar Go aplicado a servicios de producción, ingestión de telemetría y sistemas distribuidos. 22 de septiembre de 2026 - [Arquitectura](/es/articulos/?topic=arquitetura) - [Rendimiento](/es/articulos/?topic=performance) - [Mensajería](/es/articulos/?topic=mensageria) Esta guía es para quien ya trabaja con backend y necesita volver a conversar o implementar servicios Go de producción. Las decisiones que más importan son concurrencia limitada, cancelación, colas finitas, idempotencia y observabilidad. El recorte usa Go 1.26. No es una introducción al lenguaje. Repasa la sintaxis rápido y dedica el tiempo a las decisiones que cambian el diseño de un consumer, una API o un pipeline de telemetría. ## Ruta de 30 minutos Tiempo Bloque Prioridad 0-4 min Tipos, structs, interfaces y errores Revisión rápida 4-10 min Goroutines, channels, select y contexto Alta 10-17 min Concurrencia limitada y backpressure Máxima 17-23 min Pipeline IoT resiliente Máxima 23-26 min Runtime, memoria y profiling Alta 26-30 min Arquitectura y entrevista Máxima ## Fundamentos que aparecen en producción Go favorece composición, contratos pequeños y flujo explícito. Un valor puede llevar su propia validación sin depender de un framework: var ErrOutOfRange = errors.New("reading out of range") type Reading struct { DeviceID string Sequence uint64 Value float64 } func (r Reading) Validate() error { if r.DeviceID == "" { return errors.New("device_id is required") } if r.Value < -100 || r.Value > 250 { return fmt.Errorf("%w: %.2f", ErrOutOfRange, r.Value) } return nil } El zero value suele ser útil. Los slices comparten backing array hasta que append realoca; los maps no tienen orden de iteración y necesitan sincronización para acceso concurrente. Un string contiene bytes inmutables, generalmente UTF-8. defer se ejecuta en orden LIFO, pero evalúa sus argumentos al registrarse. Las interfaces se satisfacen implícitamente. Define interfaces pequeñas donde se consumen. Los errores son valores: añade contexto con %w e inspecciona la cadena con errors.Is o errors.As. Reserva panic para invariantes rotas o fallos irrecuperables de arranque. ## Goroutines, channels y contexto Una goroutine no es un hilo dedicado. El runtime la planifica sobre hilos del sistema operativo. Cada goroutine necesita owner, condición de término y alguien que espere su finalización. Los channels transfieren trabajo u ownership. Los mutexes protegen estado compartido. Un channel con buffer suaviza una diferencia temporal de velocidad; no crea capacidad infinita. func enqueue(ctx context.Context, jobs chan<- Reading, reading Reading) error { select { case jobs <- reading: return nil case <-ctx.Done(): return context.Cause(ctx) } } El productor cierra un channel cuando sabe que no habrá más envíos. Enviar a un channel cerrado o cerrarlo dos veces causa panic. Un channel nil bloquea para siempre y deshabilita su caso dentro de select. context.Context propaga cancelación, deadlines y metadatos de la request. Recíbelo primero, propágalo, llama a cada cancel retornado y no lo guardes en una struct. La cancelación es cooperativa: los loops bloqueantes deben observar ctx.Done(). ## Limita la concurrencia antes de que la memoria sea el límite Una goroutine por mensaje se vuelve costosa cuando un downstream se ralentiza. Crecen las colas y el heap, el GC trabaja más y el proceso puede caer antes de que la CPU parezca llena. errgroup combina espera, propagación del primer error y cancelación compartida. Pon un límite para tareas independientes: func ProcessBatch(ctx context.Context, batch []Reading) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(16) for _, reading := range batch { reading := reading g.Go(func() error { return processOne(ctx, reading) }) } return g.Wait() } Clasifica los errores antes de actuar. Una base indisponible puede cancelar un lote. Un payload inválido, duplicado o incompatible con el schema debe ir a cuarentena o DLQ, sin detener el consumer completo. Usa atomic para un contador o flag independiente, sync.Mutex para una invariante entre campos y channel para transferir trabajo. No copies un mutex después del primer uso ni mantengas un lock durante I/O remoto. Backpressure es una política de producto y operación. Si la ingesta recibe 50 mil mensajes por segundo y la persistencia completa 20 mil, guardar el resto en memoria solo desplaza el incidente. Decide si bloquear productor, rechazar con retry, pausar consumo para que el broker retenga backlog durable, descartar muestras antiguas, agregar datos o persistir en disco. Define tamaño de cola, métrica de ocupación, timeout y acción de saturación. ## Pipeline IoT resistente a reentregas dispositivo -> MQTT/broker -> ingesta Go -> stream -> procesadores -> almacenamiento \-> DLQ \-> estado actual MQTT encaja en la conectividad de dispositivos. Un stream como Kafka encaja en retención durable, replay y particionamiento interno. gRPC es RPC interno tipado; WebSocket actualiza dashboards. Resuelven fronteras distintas. Varios workers rompen el orden global. Telemetría suele necesitar orden por dispositivo, así que particiona por una clave estable como hash(device_id) % N y procesa cada partición de forma secuencial. Guarda observed_at, ingested_at, sequence, event_id y boot_id cuando exista. El reloj del dispositivo puede desfasarse o reiniciarse. Diseña la cadena para entrega *at least once*. Recibe el evento, valida envelope y versión del schema, comprueba la clave de idempotencia, persiste efecto y marcador de deduplicación en la misma transacción cuando sea posible y recién entonces hace ACK. Una transactional outbox cierra la ventana entre confirmar el estado en base y publicar el evento siguiente. El consumer sigue necesitando idempotencia porque las duplicatas pueden ocurrir. Retry sirve para fallos transitorios. Un payload inválido o una regla de negocio rechazada no mejora con otro intento. Añade límite, budget total y jitter para que las réplicas no repitan juntas. En el borde usa TLS, identidad por dispositivo, autorización por tópico, límite estricto de payload y validación antes de asignar estructuras grandes. Rotación, revocación, secuencia, nonce y ventana temporal importan cuando el protocolo debe resistir replay. ## Kubernetes, observabilidad y rendimiento En SIGTERM, quita readiness, deja de buscar trabajo, drena el trabajo en vuelo dentro del grace period, confirma solo los mensajes concluidos y cierra productores, conexiones y telemetría al final. Liveness pregunta si el proceso progresa y no debe depender de cada servicio externo. Readiness pregunta si ese pod puede aceptar trabajo ahora. Para escalar consumers, CPU por sí sola es una señal débil. Observa lag, edad del mensaje más antiguo, tasa de llegada, tiempo de procesamiento y ocupación del pool. Usa logs estructurados y campos de correlación sin registrar credenciales ni payloads sensibles completos. Mide throughput, errores por clase, p50/p95/p99, lag, edad del evento, retries, DLQ, duplicatas, goroutines, heap y pausas de GC. Usa tracing muestreado para cruzar ingesta, stream y persistencia; trazar cada lectura de alta frecuencia puede costar más de lo que ayuda. Una data race es acceso concurrente a la misma posición de memoria con al menos una escritura y sin orden de sincronización. Envíos por channel, unlock/lock de mutex y operaciones atómicas establecen relaciones de orden. El detector solo cubre rutas ejecutadas: go test -race ./... go test -bench=. -benchmem ./... go tool pprof cpu.out go tool trace trace.out G es goroutine, M es hilo del sistema y P es recurso lógico de ejecución. GOMAXPROCS limita cuántos Ps ejecutan código Go en --- # Goroutines vs Event Loop: la comparación equivocada entre dos modelos de concurrencia URL: https://imrafaeldev.site/es/articulos/goroutines-vs-event-loop > Concurrencia no es paralelismo. Cuándo el Event Loop de Node.js basta para I/O y cuándo las goroutines en Go encajan mejor en carga CPU bound. - [Inicio](/es/) - [Artículos](/es/articulos/) - Goroutines vs Event Loop: la comparación equivocada entre dos modelos de concurrencia ## Goroutines vs Event Loop: la comparación equivocada entre dos modelos de concurrencia Node.js con Event Loop y Go con goroutines no resuelven el mismo problema del mismo modo. El error común es confundir concurrencia con paralelismo. 24 de junio de 2026 - [Trade-offs](/es/articulos/?topic=trade-offs) - [Rendimiento](/es/articulos/?topic=performance) La tesis es simple: Node.js con Event Loop y Go con goroutines no resuelven el mismo tipo de problema del mismo modo. La comparación se vuelve mala cuando tratamos ambos como competidores directos en cualquier escenario. En la práctica, el error más común es confundir concurrencia con paralelismo. Concurrencia es organizar varias tareas que pueden estar en curso al mismo tiempo. Paralelismo es ejecutar trabajo de hecho al mismo tiempo, usando múltiples núcleos de CPU. Esa diferencia parece académica hasta que aparece en producción. El Event Loop de Node.js es muy bueno cuando el cuello de botella está en la espera: API externa, base de datos, WebSocket, input de usuario, colas y eventos. Mientras una operación aguarda respuesta, el loop sigue atendiendo otras tareas. Es el dueño de la bodega en el mostrador: no para porque pidió a alguien buscar rapadura en el depósito. Las goroutines, por otro lado, empiezan a volverse más interesantes cuando el trabajo es CPU bound, divisible y puede aprovechar múltiples núcleos con control explícito de concurrencia. Son unidades ligeras de ejecución gestionadas por el runtime de Go. Con ellas, se puede romper una tarea en partes menores, distribuir la ejecución y sincronizar el resultado al final. Es más parecido a un puesto lleno en el São João de Caruaru: una persona asa el maíz, otra revuelve la canjica, otra corta el bolo de rolo. El trabajo avanza al mismo tiempo, con cada persona cuidando una parte. ## El punto donde Node.js empieza a sufrir Lo vi de forma muy concreta en un proceso de cálculo de comisión en una casa de apuestas. La aplicación lidiaba con millones de apuestas por día, y parte del flujo implicaba calcular comisión sobre varios lotes de apuestas. Al principio, el proceso en Node.js funcionaba. Secuencialmente era correcto, pero lento. Cuando intenté paralelizar con la lógica común de varias tareas al mismo tiempo, apareció el límite: el cuello de botella era CPU. No era solo esperar base, API o evento externo. Era cálculo sobre un buffer grande de apuestas. Ese es el tipo de escenario en que Promise.all puede engañar. Da sensación de paralelismo, pero no transforma automáticamente trabajo pesado de CPU en ejecución paralela real. Si las tareas son cómputo intenso y corren en el mismo hilo principal, el Event Loop queda ocupado. El resultado puede ser peor de lo esperado: bloqueo del loop, aumento de latencia, peor responsividad y mayor presión sobre CPU y memoria. El problema no era que Node.js fuera malo. El problema era usar el modelo estándar de Node para una carga que exigía otro tipo de ejecución. ## Donde Go encajó mejor La solución fue reescribir ese proceso en Go usando goroutines. La idea era dividir el cálculo en chunks menores, procesar esos pedazos en paralelo y sincronizar solo al final. Ese diseño encajaba mejor en el problema porque el trabajo era CPU bound y podía dividirse. En lugar de un flujo centralizado intentando coordinar varias operaciones pesadas, el procesamiento pasó a distribuirse en unidades menores de ejecución. Con un worker pool, por ejemplo, se puede controlar el número de goroutines, limitar el fan-out, usar mejor los cores disponibles y evitar que el sistema dispare trabajo sin límite. La ganancia apareció. El tiempo total cayó cerca del 25%. El proceso que quedaba en torno a los 30 segundos pasó a correr en algo cercano a 22,5 segundos. También hubo mejor uso de CPU. Pero la parte importante de la historia no es “Go lo resolvió”. La parte importante es que Go resolvió un lado del problema y reveló otro. ## El cuello de botella puede cambiar de lugar La primera dificultad fue garantizar que el resultado en Go fuera igual al resultado en Node.js. Esto es menos glamoroso que hablar de concurrencia, pero es lo que separa la optimización real de la regresión enmascarada. Si el cálculo se vuelve más rápido y cambia el resultado financiero, la mejora no vale nada. Después de eso, el problema principal se volvió la división de los chunks. La estrategia inicial consumía demasiada memoria. En pruebas locales, con datasets menores, el crecimiento proporcional llegó cerca del 15% en algunos momentos. La incomodidad venía de una expectativa equivocada: pensé que cambiar a Go resolvería automáticamente el problema. En la práctica, solo había movido el cuello de botella. Antes el límite estaba más claro en CPU. Después, la estrategia de particionamiento empezó a presionar la memoria. Esto puede pasar por varios motivos: copias innecesarias, buffers grandes, slices manteniendo referencia a arrays mayores, colas internas demasiado grandes o exceso de trabajo preparado antes de procesarse. En el resultado final, la memoria aún creció cerca del 5%. En ese caso, la ganancia de tiempo y CPU compensó la pérdida. Pero esto no es una regla universal. Si la carga de producción fuera mucho mayor, o si el servicio corriera con poco margen de memoria, ese intercambio podría dejar de ser aceptable. El paralelismo cuesta coordinación, asignación, sincronización y observabilidad. No existe ejecución paralela gratis. ## Cuándo mantendría Node.js Mantendría Node.js sin incomodidad para orquestación de I/O: comunicación por WebSocket, llamadas a varias APIs, consultas en base, disparo de eventos, integración entre servicios y flujos donde el tiempo muerto está en la espera. En esos casos, el Event Loop es una excelente elección. Permite alto volumen de operaciones concurrentes sin crear un hilo por request. Para aplicaciones orientadas a eventos, esto es simple, productivo y fácil de encajar en el ecosistema JavaScript. El error es intentar empujar ese mismo modelo hacia un cálculo pesado y pensar que la concurrencia de I/O se vuelve paralelismo de CPU. No se vuelve. ## Cuándo miraría hacia Go Empezaría a mirar hacia Go cuando la tarea fuera claramente CPU bound: cálculo en alto volumen de datos, procesamiento de imagen en lote, agregaciones pesadas, compresión, transformación grande de buffers, simulaciones o cualquier rutina en que la máquina pase más tiempo calculando que esperando respuesta externa. En ese tipo de escenario, las goroutines con worker pool dan mejor control sobre el uso de CPU. También dejan más explícita la separación entre unidades de trabajo, sincronización y recolección de resultado. Pero Go también cobra precio. Hay que pensar en granularidad de los chunks, consumo de memoria, cancelación, tratamiento de error, backpressure, límites de workers, contención y consistencia del resultado. Si la división del trabajo es ingenua, la ganancia de CPU puede venir acompañada de estallido de memoria o complejidad innecesaria. ## Contraargumento: Node.js también tiene worker threads Existe un contraargumento justo: Node.js no está limitado al Event Loop para todo. Los worker threads existen justamente para ejecutar trabajo pesado fuera del hilo principal. También hay estrategias con colas, procesos separados, servicios auxiliares y native addons. Entonces la comparación honesta no es “Node.js no puede”. Puede. La cuestión es costo de implementación, madurez del equipo, observabilidad, integración con el sistema existente y cuánto esfuerzo vale invertir para mantener ese procesamiento dentro del ecosistema Node. En algunos equipos, usar worker threads puede bastar y ser más barato que introducir Go. En otros, separar el procesamiento CPU bound en un servicio Go puede ser más simple de operar y escalar. La decisión no debería nacer de preferencia por --- # Artículos URL: https://imrafaeldev.site/es/articulos > Patrones y decisiones de ingeniería en prosa técnica, con código. - [Inicio](/es/) - Artículos ## Artículos Patrones y decisiones de ingeniería en prosa técnica, con código. TodosRendimientoArquitecturaTrade-offsMensajería - ## [Intensivo de Golang: concurrencia, resiliencia y sistemas distribuidos en 30 minutos](/es/articulos/go-intensivo/) 22 de septiembre de 2026 [Arquitectura](/es/articulos/?topic=arquitetura) - [Rendimiento](/es/articulos/?topic=performance) - [Mensajería](/es/articulos/?topic=mensageria) Una guía de 30 minutos para reactivar Go aplicado a servicios de producción, ingestión de telemetría y sistemas distribuidos. - ## [Desenoxidando la lógica #02: Container With Most Water](/es/articulos/desenoxidando-logica-container-with-most-water/) 15 de septiembre de 2026 [Rendimiento](/es/articulos/?topic=performance) - [Trade-offs](/es/articulos/?topic=trade-offs) Intenté elegir el próximo paso mirando solo a los vecinos. Funcionó en algunos casos, pero el problema pedía una visión más amplia. - ## [Desenoxidando la lógica #01: Group Anagrams](/es/articulos/desenoxidando-logica-group-anagrams/) 17 de agosto de 2026 [Trade-offs](/es/articulos/?topic=trade-offs) - [Rendimiento](/es/articulos/?topic=performance) Yo no había dejado de escribir código. Lo que cambió fue tercerizar partes del razonamiento. Group Anagrams fue el ejercicio para recuperar el hábito. - ## [La mayoría de los problemas de performance de backend empieza cerca de los datos](/es/articulos/backend-performance-cerca-de-los-datos/) 16 de julio de 2026 [Rendimiento](/es/articulos/?topic=performance) - [Arquitectura](/es/articulos/?topic=arquitetura) API lenta y la reunión ya se llena de soluciones. Yo empiezo cerca de los datos — no por culpar a la base, sino porque esa verificación suele dar señal rápida. - ## [Goroutines vs Event Loop: la comparación equivocada entre dos modelos de concurrencia](/es/articulos/goroutines-vs-event-loop/) 24 de junio de 2026 [Trade-offs](/es/articulos/?topic=trade-offs) - [Rendimiento](/es/articulos/?topic=performance) Node.js con Event Loop y Go con goroutines no resuelven el mismo problema del mismo modo. El error común es confundir concurrencia con paralelismo. - ## [TypeScript Clean Architecture: Core, Adapters e Infra](/es/articulos/typescript-clean-architecture/) 15 de marzo de 2023 [Arquitectura](/es/articulos/?topic=arquitetura) Arquitectura débil traba mantenimiento, pruebas y cambio. Esta derivación de Clean Architecture para backend TypeScript separa Core, Adapters e Infra — con dependencias apuntando hacia dentro. - ## [Design Patterns: Strategy](/es/articulos/design-patterns-strategy/) 5 de mayo de 2022 [Arquitectura](/es/articulos/?topic=arquitetura) Las cadenas de if/else crecen y se vuelven frágiles. Strategy aísla cada algoritmo tras un contrato. - ## [Deja de ser rehén de las dependencias. Saluda al patrón de diseño Adapter](/es/articulos/design-patterns-adapter/) 26 de abril de 2022 [Arquitectura](/es/articulos/?topic=arquitetura) Migrar de MySQL a PostgreSQL no exige reescribir el servicio. El Adapter aísla el plugin tras un contrato que la regla de negocio entiende. --- # TypeScript Clean Architecture: Core, Adapters e Infra URL: https://imrafaeldev.site/es/articulos/typescript-clean-architecture > Derivación de Clean Architecture para backend TypeScript: Core con usecases y protocols, Adapters bidireccionales e Infra NestJS con inyección de dependencias. - [Inicio](/es/) - [Artículos](/es/articulos/) - TypeScript Clean Architecture: Core, Adapters e Infra ## TypeScript Clean Architecture: Core, Adapters e Infra Arquitectura débil traba mantenimiento, pruebas y cambio. Esta derivación de Clean Architecture para backend TypeScript separa Core, Adapters e Infra — con dependencias apuntando hacia dentro. 15 de marzo de 2023 - [Arquitectura](/es/articulos/?topic=arquitetura) El desarrollo de software cambia todo el tiempo. La arquitectura débil se vuelve mantenimiento caro, feature lenta, prueba difícil y bug difícil de aislar. Vale invertir en una estructura que soporte evolución sin reescribir el sistema ante cada presión del negocio. ## Un poco de historia Clean Architecture es el nombre que Robert C. Martin (Uncle Bob) dio, en 2012, en el libro *Clean Architecture: A Craftsman’s Guide to Software Structure and Design*. La propuesta huye de la rigidez de arquitecturas acopladas a framework y base: el núcleo queda estable; los detalles externos cambian. La idea bebe de DDD, SOLID, Onion Architecture y Hexagonal Architecture. ## Propuesta general Este artículo describe Clean Architecture y una derivación práctica para backends en TypeScript: tres capas — **Core**, **Adapters** e **Infra**. - **Core** — regla de negocio y entidades del dominio. Capa más interna. - **Infra** — conexiones externas: repositorios concretos, controllers REST, módulos de DI, boilerplate de framework. - **Adapters** — intermediación en ambos sentidos. El controller no llama al usecase “crudo”: pasa por un servicio. El usecase no habla con la base: habla con un protocolo que un adapter (repositorio, connector, handler) implementa. Cada capa tiene capacidades y restricciones distintas; SOLID pesa más en el Core. Sirve para CRUD HTTP y para sistemas con varios frameworks y canales. Beneficios concretos: responsabilidades claras (lectura y mantenimiento), flexibilidad para cambiar plugin sin reescribir regla, y pruebas aisladas por capa. ## Guía de capas Ejemplo: CRUD de usuarios vía REST con NestJS. Detalles de instalación quedan fuera. Escritura **core-to-infra** (de dentro hacia fuera). ## Core En el diseño clásico, *domain* y *entities* quedan muy próximas. Aquí forman el **Core**: todo lo que la regla de negocio *es* — funcionalidades y representaciones del dominio. En el ejemplo, la entidad principal es Usuario (id, name), en core/entities. ### Entities // core/entities/UserEntity.ts export interface UserEntityProps { id?: string; name: string; } export class UserEntity { constructor(private readonly props: UserEntityProps) {} get id(): string { return this.props.id ?? ""; } get name(): string { return this.props.name; } } La entidad recibe props tipadas y expone getters. Depende de una interfaz que cualquier DTO de transferencia puede satisfacer después. ### Features y usecases El CRUD necesita crear, buscar, actualizar y eliminar. En el Core, cada usecase implementa un contrato (feature) con un único método público — alineado a Liskov, abierto/cerrado, segregación de interfaz y responsabilidad única. El usecase **no** accede a la base: conoce **protocols** que describen la acción externa (inversión de dependencia). Registro: nombre obligatorio; si ya existe, error; si no, retorna UserEntity. - contrato CreateUser - implementación CreateUserUsecase En TypeScript, clase abstracta con métodos abstractos funciona como contrato *y* valor — útil para DI (const createUserSymbol = CreateUser): // core/features/CreateUser.ts export abstract class CreateUser { abstract execute(name: string): Promise<UserEntity>; } // core/usecases/CreateUserUsecase.ts export class CreateUserUsecase implements CreateUser { constructor( private readonly createUserProtocol: CreateUserProtocol, private readonly getByNameProtocol: GetUserByNameProtocol, ) {} async execute(name: string): Promise<UserEntity> { const existsName = await this.getByNameProtocol.getByName(name); if (existsName) { throw new UserAlreadyExistsException( `the name ${name} already exists`, ); } return this.createUserProtocol.register(name); } } El usecase define *qué* (validar nombre, registrar). No define *cómo* buscar o persistir. La regla queda independiente de lib, framework y base. Cuidado: un usecase que solo delega al protocol sin validar puede estar empujando regla de negocio hacia el adapter. En CreateUserUsecase, la verificación de nombre duplicado es obligación del Core. ### Exceptions UserAlreadyExistsException pertenece al Core: el flujo inválido de la regla también es regla. Cada fallo mapeado a una excepción conocida ayuda al mantenimiento. Base con code (después se vuelve status HTTP en el borde): // core/exceptions/IBaseException.ts export abstract class IBaseException extends Error { code: number; constructor(message: string) { super(message); } } // core/exceptions/UserAlreadyExistsException.ts export class UserAlreadyExistsException extends IBaseException { constructor(message?: string) { super(message ?? "User already exists"); this.code = 400; } } El usecase **lanza** excepciones; **no** las trata. Mapear tipo desconocido → tipo conocido queda en adapter o infra. ### Protocols CreateUserProtocol y GetUserByNameProtocol son contratos de acceso a dispositivo externo. El protocol existe para informar o disparar acción externa — **no** para procesar regla de negocio. Preferencia: un método público por protocol. // core/protocols/CreateUserProtocol.ts export abstract class CreateUserProtocol { abstract register(name: string): Promise<UserEntity>; } // core/protocols/GetUserByNameProtocol.ts export abstract class GetUserByNameProtocol { abstract getByName(name: string): Promise<UserEntity | null>; } El Core es el centro; la capa de adaptación conecta el resto. ## Adapter Los adapters controlan el tráfico bidireccional: externo → regla y regla → externo. Adaptan objetos, parámetros y excepciones — el mismo espíritu del [patrón Adapter](/es/articulos/design-patterns-adapter/). Dos grupos: - Llamados por el Core — implementan al menos un protocol. - Llamados por la Infra — en general **services**. ### Connectors, handlers y repositories Clases que implementan protocols. Cada una adapta **un** dispositivo externo (ORM, cliente HTTP, cola, filesystem). Convención de nombres: - **Repositories** — protocol ligado a base (vocabulario familiar). - **Connectors** — retornan datos sin ser “tabla” (ej.: ClientHttpFetchConnector, ClientHttpAxiosConnector). - **Handlers** — procesan sin retorno síncrono (ej.: publicar en Kafka). Otros nombres son válidos; el criterio es un adapter por dispositivo. En el CRUD, solo repository (mock): // adapters/repositories/UsersMockRepository.ts export class UsersMockRepository implements GetUserByIdProtocol, GetUserByNameProtocol, CreateUserProtocol, UpdateUserProtocol, DeleteUserProtocol { private db: DbConnector; constructor() { this.db = mockDbConnector; } async getById(id: string): Promise<UserEntity> { return this.db.users.getById(id); } async getByName(name: string): Promise<UserEntity | null> { return this.db.users.getByName(name); } async register(name: string): Promise<UserEntity> { return this.db.users.register(name); } async update(id: string, name: string): Promise<UserEntity> { return this.db.users.update(id, name); } async delete(id: string): Promise<void> { return this.db.users.delete(id); } } Mock del conector: export const mockDbConnector: DbConnector = { users: { getById: async (id: string) => Promise.resolve(new UserEntity({ id, name: "Test" })), getByName: async (name: string) => Promise.resolve(new UserEntity({ id: "1", name })), register: async (name: string) => Promise.resolve(new UserEntity({ id: "2", name })), update: async (id: string, name: string) => Promise.resolve(new UserEntity({ id, name })), delete: async (_id: string) => Promise.resolve(), }, profiles: { getById: async (_id: string) => --- # VBET: analítica sobre un SQL Server que no podíamos cambiar URL: https://imrafaeldev.site/es/casos/analitica-sql-server-externo > Dashboard de comisiones en VBET: SQL Server externo, ETL propio y caché. La línea medida fue de cerca de siete minutos en el pico hasta menos de un segundo con caché caliente. - [Inicio](/es/) - [Casos](/es/casos/) - VBET: analítica sobre un SQL Server que no podíamos cambiar VBET ## VBET: analítica sobre un SQL Server que no podíamos cambiar La base era de otro equipo. El dashboard necesitaba dejar de depender de un schema que no controlábamos. Ingeniero Backend Sénior oct/2023 – feb/2025 ~7 min → <1 s comisiones, de la carga original a la caché caliente Pipeline de comissões Cada estágio corresponde a uma decisão incremental documentada no case. Números de outras histórias não entram neste desenho. - [01Contexto](#contexto) - [02Restricciones](#restricciones) - [03Problema](#problema) - [04Decisión](#decision) - [05Alternativa descartada](#alternativa-descartada) - [06Resultado](#resultado) - [07Limitaciones](#limitaciones) ## Contexto En VBET, entre octubre de 2023 y febrero de 2025, el producto de analítica servía a influencers y afiliados de iGaming. El dashboard reunía decenas de métricas; la comisión era la lectura más crítica. Los influencers aceptaban un pequeño desfase en los datos del día, siempre que la pantalla respondiera. El pago dependía de datos consolidados del día anterior, no del valor en vivo. ## Restricciones El SQL Server era externo, compartido y no modificable de forma fiable. Los índices temporales podían ser eliminados por el propietario de la base. La API original mezclaba consultas SQL construidas a partir de parámetros, con riesgo de inyección, y agregaba demasiado en memoria. ## Problema El sistema había sido dimensionado para influencers más pequeños. Con bases mayores, el peor pico del dashboard llegó a cerca de siete minutos. Seguridad y mantenibilidad vinieron antes del rendimiento: queries crudas, poca cobertura de pruebas y un camino síncrono que recalculaba demasiado en cada request. ## Decisión La evolución fue incremental, en el orden en que aparecieron las restricciones: - eliminar SQL inseguro, parametrizar el acceso, documentar y probar; - optimizar queries e índices temporales, como mitigación y no como invariante; - paralelizar consultas independientes con Go, goroutines y channels; - cuando el cuello de botella volvió al SQL Server, crear ETL y PostgreSQL propios, con precálculo, checkpoints y reconciliación; - separar lectura REALTIME (tendencia, consistencia eventual) de CLOSED (precisión financiera y pago); - cache-aside con TTL alineado al desfase aceptado de cerca de cinco minutos; - degradación controlada si fallaba la caché, en lugar de tumbar la pantalla. ## Alternativa descartada Insistir en índices en la base externa como arquitectura, o recalcular años de historial en cada acceso. También se descartó la idea de pagar al afiliado con el dato REALTIME. ## Resultado La línea reconciliada en el dosier de la experiencia, para el camino de comisión/dashboard, es: - peor pico inicial: cerca de 7 minutos; - tras queries e índices: cerca de 3 minutos; - tras paralelización: cerca de 1 minuto; - tras ETL/PostgreSQL: cerca de 15 segundos en el p99 de la comisión sin caché; - caché caliente: menos de 1 segundo. Cada número pertenece a esa etapa. No describe la ganancia de una descomposición posterior en microservicios. ## Limitaciones La investigación, la decisión de ETL + base propia, la separación REALTIME/CLOSED y la política de degradación son el núcleo atribuible aquí. La descomposición del monolito en Kubernetes es otra historia y no mezcla el “500%” ni la latencia por debajo de 60 ms con este caso. Versiones antiguas de currículo que citan 30 segundos en carga fría, 11 segundos o porcentajes de SLA sin escenario quedan fuera. Si el producto pasa a exigir precisión realtime en el pago, la separación CLOSED deja de ser suficiente. Contacto ## ¿Tienes un sistema que dejó de ser simple? Escríbeme directo por @imrafaeldev, sin formulario — para conversación profesional, empieza en LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Abrir la página de contacto](/es/contacto/) --- # Estudios de caso URL: https://imrafaeldev.site/es/casos > Cada case documenta la restricción, la decisión, la alternativa descartada y el resultado medido. También indica cuándo conviene revisar la decisión. - [Inicio](/es/) - Casos ## Estudios de caso Cada case documenta la restricción, la decisión, la alternativa descartada y el resultado medido. También indica cuándo conviene revisar la decisión. - Infosistemas feb/2025 – may/2026 ## [Infosistemas: contrato de fallo en la mensajería RabbitMQ](/es/casos/mensajeria-rabbitmq/) Fallos intermitentes entre microservicios sin contrato para retry, DLQ o duplicidad. Más consumidores solo desplazaban la sobrecarga. **~98%** reducción de fallos intermitentes en los flujos críticos [Abrir el case — Infosistemas: contrato de fallo en la mensajería RabbitMQ](/es/casos/mensajeria-rabbitmq/) - VBET oct/2023 – feb/2025 ## [VBET: analítica sobre un SQL Server que no podíamos cambiar](/es/casos/analitica-sql-server-externo/) La base era de otro equipo. El dashboard necesitaba dejar de depender de un schema que no controlábamos. **~7 min → <1 s** comisiones, de la carga original a la caché caliente [Abrir el case — VBET: analítica sobre un SQL Server que no podíamos cambiar](/es/casos/analitica-sql-server-externo/) - Flapper sep/2021 – jun/2022 ## [Flapper: descubrir el dominio antes de separar el monolito](/es/casos/modernizacion-monolito-sin-documentacion/) El producto no podía parar, y los autores originales ya no estaban. Antes de migrar, fue preciso descubrir qué fronteras aún revelaba la base. **~25%** menos tablas en la separación por dominios [Abrir el case — Flapper: descubrir el dominio antes de separar el monolito](/es/casos/modernizacion-monolito-sin-documentacion/) --- # Infosistemas: contrato de fallo en la mensajería RabbitMQ URL: https://imrafaeldev.site/es/casos/mensajeria-rabbitmq > Rediseño de la mensajería RabbitMQ en Infosistemas con colas durables, DLQ, retry, idempotencia y prefetch. El trabajo redujo en cerca del 98% los fallos intermitentes entre microservicios. - [Inicio](/es/) - [Casos](/es/casos/) - Infosistemas: contrato de fallo en la mensajería RabbitMQ Infosistemas ## Infosistemas: contrato de fallo en la mensajería RabbitMQ Fallos intermitentes entre microservicios sin contrato para retry, DLQ o duplicidad. Más consumidores solo desplazaban la sobrecarga. Ingeniero de Software Sénior / Arquitecto de Software feb/2025 – may/2026 ~98% reducción de fallos intermitentes en los flujos críticos Contrato de falha na mensageria Publicação confirmada, consumo com prefetch controlado, retry com backoff e DLQ por fluxo. Escalar só consumidores fica de fora do desenho. - [01Contexto](#contexto) - [02Restricciones](#restricciones) - [03Problema](#problema) - [04Decisión](#decision) - [05Alternativa descartada](#alternativa-descartada) - [06Implementación](#implementacion) - [07Resultado](#resultado) - [08Limitaciones](#limitaciones) ## Contexto Infosistemas opera plataformas de gestión para arrendadoras, flotas y automotrices. El trabajo ocurrió en el equipo de arquitectura, en colaboración con DevOps, SREs y DBAs, entre febrero de 2025 y mayo de 2026. Este caso cubre el frente de mensajería. Otros frentes de la misma experiencia (seguridad del ERP, jornadas de firma, integraciones fiscales) existen en las fuentes, pero no entran aquí como número o afirmación extra. ## Restricciones Los flujos críticos cruzaban microservicios. El fallo era intermitente: la misma operación podía completarse en una ejecución y no completarse en la siguiente. Aumentar concurrencia o prefetch sin criterio transfería sobrecarga a consumidores, servicios o bases downstream. ## Problema Los mensajes dejaban de completar el flujo esperado. Investigar un fallo parcial era difícil. No había contrato explícito para fallo temporal, fallo permanente, duplicidad o poison message. ## Decisión La mensajería fue rediseñada para volver predecible el comportamiento ante fallos: - colas durables; - DLQ por flujo, para el mensaje que no debe desaparecer ni repetirse sin control; - retry con backoff para indisponibilidad temporal; - idempotencia y deduplicación en el consumidor, porque la entrega duplicada no puede repetir el efecto de negocio; - publisher confirms, para reducir la incertidumbre en la publicación; - ajuste de prefetch, en lugar de abrir concurrencia indiscriminada. ## Alternativa descartada Tratar el problema como falta de capacidad (más consumidores, más prefetch) sin cambiar el contrato de fallo. Eso movería el cuello de botella y mantendría pérdida o duplicidad silenciosa. ## Implementación El rediseño colocó estos mecanismos en los flujos críticos: publicación confirmada, cola durable con prefetch controlado, consumidor idempotente, retry con backoff y DLQ por flujo. La operación pasó a tener un camino predecible para el fallo temporal y para el fallo permanente, en lugar de depender de reprocesamiento ad hoc. ## Resultado La reducción registrada en los fallos intermitentes de los flujos críticos entre microservicios fue de aproximadamente el 98%. El número describe esos flujos tras el rediseño, no la operación entera de la empresa ni otros frentes. ## Limitaciones Las métricas de otros frentes aún pendientes de método o confirmación quedan fuera. Si la volumetría o el mapa de microservicios cambia de forma que DLQ y prefetch dejen de aislar el fallo, el tuning debe revisarse con telemetría de colas y de consumidores. Contacto ## ¿Tienes un sistema que dejó de ser simple? Escríbeme directo por @imrafaeldev, sin formulario — para conversación profesional, empieza en LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Abrir la página de contacto](/es/contacto/) --- # Flapper: descubrir el dominio antes de separar el monolito URL: https://imrafaeldev.site/es/casos/modernizacion-monolito-sin-documentacion > Modernización incremental de un monolito PHP sin documentación en Flapper: base como fuente de descubrimiento, bounded contexts y Strangler. La separación por dominios redujo en aproximadamente el 25% el número de tablas. - [Inicio](/es/) - [Casos](/es/casos/) - Flapper: descubrir el dominio antes de separar el monolito Flapper ## Flapper: descubrir el dominio antes de separar el monolito El producto no podía parar, y los autores originales ya no estaban. Antes de migrar, fue preciso descubrir qué fronteras aún revelaba la base. Ingeniero de Software Full Stack sep/2021 – jun/2022 ~25% menos tablas en la separación por dominios Descoberta de domínio antes da migração O banco legado revela fronteiras; pessoas, autenticação e aeronaves saem gradualmente para contextos com persistência própria. - [01Contexto](#contexto) - [02Restricciones](#restricciones) - [03Problema](#problema) - [04Decisión](#decision) - [05Alternativa descartada](#alternativa-descartada) - [06Resultado](#resultado) - [07Limitaciones](#limitaciones) ## Contexto En Flapper, el producto principal era un monolito PHP con más de siete años, poca documentación útil y sin los desarrolladores que lo habían creado. La aplicación sostenía la operación de aviación ejecutiva y no podía interrumpirse para una reescritura. ## Restricciones El código y la base acumulaban reglas y dependencias difíciles de explicar. Los cambios tenían efectos colaterales poco predecibles, y no había especialistas remanentes para confirmar cómo cada parte del sistema debía evolucionar. La migración necesitaba coexistir con el producto en producción. ## Problema Cambiar PHP por otra tecnología no respondería la duda principal: qué reglas pertenecían juntas y qué dependencias podían separarse sin romper la operación. El sistema necesitaba fronteras de dominio antes de servicios nuevos. ## Decisión Usé la base y el código existente como fuente de descubrimiento. Agrupamientos de tablas y relaciones ayudaron a identificar bounded contexts; a partir de ellos, la migración siguió el patrón Strangler: - extraer un dominio por vez, sin interrumpir el monolito; - mantener en cada contexto solo la representación local de los datos que necesitaba; - propagar cambios por eventos en Kafka, en lugar de conectar todos los servicios a la base antigua; - usar Node.js, NestJS y Go en los primeros módulos, con gRPC, REST o GraphQL según el consumidor; - documentar la estrategia y los primeros módulos para que el equipo pudiera continuar la transformación. ## Alternativa descartada Reescribir el monolito entero, o mantener servicios nuevos atados a la misma base y a las mismas relaciones compartidas. La primera opción pararía el negocio; la segunda preservaría el acoplamiento que la migración necesitaba reducir. ## Resultado La separación por dominios redujo en aproximadamente el 25% el número de tablas y creó siete bases organizadas por contexto. Los módulos de personas, autenticación y aeronaves fueron los primeros pasos de una transformación planeada para continuar más allá de la entrega inicial. ## Limitaciones El número mide la reducción de tablas en esa separación de dominios, no una ganancia financiera, la migración completa o un resultado de mercado de la empresa. La fecha final de la experiencia tiene divergencia histórica en fuentes antiguas; el período publicado sigue el perfil exportado. Si un dominio aún depende de reglas no mapeadas en el monolito, su extracción debe posponerse o recibir una integración de transición explícita. Contacto ## ¿Tienes un sistema que dejó de ser simple? Escríbeme directo por @imrafaeldev, sin formulario — para conversación profesional, empieza en LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/)[YouTube](https://www.youtube.com/@imrafaeldev)[GitHub](https://github.com/imrafaeldev)[LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Abrir la página de contacto](/es/contacto/) --- # Contacto URL: https://imrafaeldev.site/es/contacto > Habla con Rafael Pereira vía @imrafaeldev en LinkedIn, GitHub, Instagram y YouTube. Sin formulario: elige el canal y escribe directamente. - [Inicio](/es/) - Contacto Contacto ## ¿Tienes un sistema que dejó de ser simple? Escríbeme directo por @imrafaeldev, sin formulario. Para conversación profesional, empieza en LinkedIn; para ver decisiones en código, ve a GitHub. - [Instagram @imrafaeldev](https://www.instagram.com/imrafaeldev/) - [YouTube @imrafaeldev](https://www.youtube.com/@imrafaeldev) - [GitHub @imrafaeldev](https://github.com/imrafaeldev) - [LinkedIn @imrafaeldev](https://www.linkedin.com/in/imrafaeldev/) --- # Currículum URL: https://imrafaeldev.site/es/curriculum > Currículum online de Rafael Pereira, ingeniero backend senior con experiencia en Node.js, Go y Java, y trabajo complementario con React y Angular. - [Inicio](/es/) - Currículum ## Ingeniería para sistemas que dejaron de ser simples Ingeniero Backend Senior Ingeniero Backend Senior con más de siete años de experiencia en sistemas distribuidos, plataformas transaccionales y modernización de legados. Combino ejecución práctica con arquitectura, liderazgo técnico, mentoría y colaboración con producto y stakeholders. Mi eje principal es Node.js, Go y Java; React y Angular aparecen como trabajo complementario en productos que necesitan continuidad entre backend y frontend. ## Experiencia - feb/2025 a may/2026 Remoto Infosistemas Ingeniero de Software Senior / Arquitecto de Software Expandir experiencia Replegar experiencia Lideré integraciones en NestJS y Go y rediseñé flujos entre microservicios, reduciendo aproximadamente un 98% las fallas intermitentes en flujos críticos. Contexto Trabajé en plataformas de gestión para arrendadoras, flotas y automotrices, dentro del equipo de arquitectura y junto a especialistas de operación y datos. Contribución Lideré integraciones en NestJS y Go y rediseñé flujos entre microservicios, haciendo más predecible el comportamiento ante fallos en los flujos críticos. [Leer la experiencia completa](/es/experiencias/infosistemas/) - jul/2025 a dic/2025 Remoto EDS (Policía Civil de Río de Janeiro) Ingeniero Backend — Consultoría Expandir experiencia Replegar experiencia Estructuré un sistema de gestión de salud con NestJS para una operación pública crítica y evolucioné rutas backend con foco en seguridad, acceso y trazabilidad. Contexto La consultoría cubrió sistemas públicos sensibles, incluyendo un sistema de gestión de salud de alto volumen y un ERP jurídico en evolución. Contribución Estructuré el backend con NestJS, refactoricé rutas legadas y reforcé la seguridad, el control de acceso y la trazabilidad de flujos sensibles. [Leer la experiencia completa](/es/experiencias/eds-policia-civil-rio/) - mar/2025 a jun/2025 Remoto Azify Ingeniero Backend Senior — Consultoría Expandir experiencia Replegar experiencia Desarrollé un motor de liquidación con NestJS y reduje un 30% la latencia de APIs financieras críticas mediante profiling y optimización. Contexto Trabajé en fintech y criptoactivos, en servicios financieros donde la consistencia, la seguridad y la estabilidad tenían impacto directo en la operación. Contribución Desarrollé un motor de liquidación con NestJS e integraciones financieras y reduje un 30% la latencia de APIs críticas mediante profiling y optimización. [Leer la experiencia completa](/es/experiencias/azify/) - oct/2023 a feb/2025 Remoto VBET Ingeniero Backend Senior Expandir experiencia Replegar experiencia Apliqué Go, goroutines y channels para reducir la primera etapa del cálculo de comisiones de cerca de siete a tres minutos, y también contribuí a la aplicación React. Contexto El producto de analítica atendía a influencers y afiliados de iGaming; su dashboard reunía métricas de comisión y aceptaba un pequeño desfase para los datos del día, mientras que los pagos requerían datos consolidados. Contribución Reestructuré la API y el cálculo con Go, goroutines y channels, introduje ETL, precálculo y caché y colaboré en la aplicación React. La línea medida pasó de cerca de siete minutos en el pico a cerca de tres minutos, cerca de un minuto, aproximadamente 15 segundos sin caché y menos de un segundo con caché caliente, cada valor correspondiente a su etapa. [Leer la experiencia completa](/es/experiencias/vbet/) - abr/2023 a oct/2023 Remoto Maxmilhas Ingeniero de Software Full Stack Expandir experiencia Replegar experiencia Desarrollé microservicios en Node.js y NestJS para automatizar cancelaciones y cambios, reduciendo un 34% la intervención manual del soporte. Contexto Trabajé en flujos posventa de viajes, incluyendo cancelaciones, cambios, cupones y comunicación con clientes. Contribución Desarrollé microservicios en Node.js y NestJS y automaticé reglas, cálculos y validaciones posventa; el cambio redujo un 34% la necesidad de intervención manual del soporte. [Leer la experiencia completa](/es/experiencias/maxmilhas/) - jun/2022 a abr/2023 Remoto South System (asignado a QUIQ/Itaú) Ingeniero Backend Expandir experiencia Replegar experiencia Diseñé un marketplace multi-tenant con Node.js y servicios asíncronos en Go, estructurando pruebas críticas con aproximadamente un 95% de cobertura. Contexto El trabajo se enfocó en un marketplace white-label y multi-tenant para instituciones financieras, preparado para incorporar nuevos bancos sin forks por cliente. Contribución Participé en la arquitectura y el refinamiento de reglas, usando Node.js y servicios asíncronos en Go para aislar variaciones por tenant; estructuré pruebas críticas con aproximadamente un 95% de cobertura. [Leer la experiencia completa](/es/experiencias/south-system-quiq-itau/) - sep/2021 a jun/2022 Remoto Flapper Ingeniero de Software Full Stack Expandir experiencia Replegar experiencia Migré módulos de personas, autenticación y aeronaves a servicios en Node.js, NestJS y Go; la separación por dominios redujo aproximadamente un 25% el número de tablas. Contexto La plataforma de aviación ejecutiva dependía de un monolito de más de siete años, con poca documentación y reglas difíciles de separar sin interrumpir la operación. Contribución Mapeé fronteras de dominio y conduje una modernización incremental, migrando módulos a servicios en Node.js, NestJS y Go; la separación redujo aproximadamente un 25% el número de tablas. [Leer la experiencia completa](/es/experiencias/flapper/) - ene/2021 a ago/2021 Remoto Sustentec Ingeniero de Software Full Stack Pleno Expandir experiencia Replegar experiencia Mantuve y evolucioné un sistema de gestión de laboratorios en Java, Spring Boot y Angular, implementando pruebas de integración antes inexistentes. Contexto Trabajé en sistemas vinculados a laboratorios, investigación y desarrollo, combinando el mantenimiento de un producto existente con su evolución funcional. Contribución Mantuve y evolucioné el sistema con Java, Spring Boot y Angular, implementé pruebas de integración y entregué informes y funcionalidades de extremo a extremo. [Leer la experiencia completa](/es/experiencias/sustentec/) - nov/2019 a ene/2021 Campina Grande, Paraíba / Remoto Braistech Ingeniero de Software Full Stack Pleno Expandir experiencia Replegar experiencia Lideré la estructuración del sistema principal con Node.js y NestJS y diseñé microservicios para el núcleo del negocio. Contexto En un producto con contratos de criptoactivos y movimientos financieros, asumí responsabilidad amplia dentro de un equipo pequeño. Contribución Lideré la estructuración del sistema principal con Node.js y NestJS, diseñé microservicios para el núcleo del negocio, ayudé a evolucionar el código hacia Clean Architecture y orienté a desarrolladores junior. [Leer la experiencia completa](/es/experiencias/braistech/) ## Especialidades - ### Node.js Especialidad en APIs, microservicios e integraciones con NestJS. - ### Go Experiencia avanzada en APIs, concurrencia y procesamiento de alto volumen. - ### Java Experiencia en mantenimiento y evolución de sistemas con Spring Boot. - ### React Trabajo en la evolución y el mantenimiento de aplicaciones React. - ### Angular Trabajo en una aplicación web Angular con formularios, validaciones y consumo de API. ## Formación - ### Análisis y Desarrollo de Sistemas Tecnólogo · UNOPAR — Universidade Norte do Paraná finalizado en 2024 - ### Computación en la Nube Posgrado · Anhanguera Educacional finalizado en 2025 ## Enlaces públicos - [LinkedIn — Abrir enlace público](https://www.linkedin.com/in/this-rafael-pereira/) - [GitHub — Abrir enlace público](https://github.com/this-rafael) --- # Azify URL: https://imrafaeldev.site/es/experiencias/azify > Mi consultoría en Azify con liquidación, BaaS y servicios financieros. - [Inicio](/es/) - Azify ## Azify Mi consultoría en Azify con liquidación, BaaS y servicios financieros. Cargo Ingeniero Backend Senior — Consultoría Período mar/2025 a jun/2025 ## Contexto En Azify, trabajé como consultor en infraestructura financiera para fintechs y bancos pequeños. El producto reunía capacidades como Pix, transferencias, tarjetas, billetera digital y otros servicios bancarios. ## Cómo trabajé Participé en decisiones arquitectónicas y ayudé a establecer prácticas de desarrollo para un entorno donde consistencia, seguridad y estabilidad tenían impacto financiero directo. Desarrollé un motor de liquidación en NestJS con integraciones multi-exchange, monitoreo de riesgo y controles de compliance. También trabajé en integraciones de exchanges y blockchains para flujos transaccionales y en una plataforma BaaS multi-tenant con OAuth 2.0, JWT y cifrado. Mediante profiling de consultas, revisión de índices y optimización de Redis, reduje en 30% la latencia de APIs financieras críticas. ## Lo que me llevé Esta consultoría retomó partes recurrentes de mi trayectoria, como pagos, multi-tenancy y sistemas transaccionales, en un contexto con mayor responsabilidad sobre autorización y consistencia. --- # Braistech URL: https://imrafaeldev.site/es/experiencias/braistech > Mi experiencia en Braistech con producto, microservicios y criptoactivos. - [Inicio](/es/) - Braistech ## Braistech Mi experiencia en Braistech con producto, microservicios y criptoactivos. Cargo Ingeniero de Software Full Stack Pleno Período nov/2019 a ene/2021 ## Contexto En Braistech, tuve una de mis primeras experiencias de producto en un entorno pequeño, con pocas personas y responsabilidad distribuida. El dominio involucraba contratos de criptoactivos y movimientos financieros. ## Cómo trabajé Lideré la estructuración del sistema principal con Node.js y NestJS, participé en el diseño de microservicios para el núcleo del negocio y desarrollé aplicaciones Flutter. También construí un sistema de contratos y trabajé en integraciones de pago relacionadas con el ecosistema de Binance. Participé además en la transición de una organización MVC hacia Clean Architecture. El objetivo era reducir acoplamiento y facilitar el mantenimiento de un sistema en crecimiento, mientras orientaba a desarrolladores junior en las decisiones de código. ## Lo que me llevé Esta etapa consolidó mi interés por backend y arquitectura. Trabajar en todo el producto también me dio una visión full stack que sigue siendo útil en conversaciones con frontend y producto. --- # EDS y Policía Civil de Río de Janeiro URL: https://imrafaeldev.site/es/experiencias/eds-policia-civil-rio > Mi experiencia de consultoría en sistemas públicos sensibles. - [Inicio](/es/) - EDS (Policía Civil de Río de Janeiro) ## EDS (Policía Civil de Río de Janeiro) Mi experiencia de consultoría en sistemas públicos sensibles. Cargo Ingeniero Backend — Consultoría Período jul/2025 a dic/2025 ## Contexto En mi consultoría para EDS, trabajé en sistemas destinados a la Policía Civil de Río de Janeiro. El contexto incluía una operación pública crítica, un sistema de gestión de salud de alto volumen y un ERP jurídico en evolución. ## Cómo trabajé Estructuré el backend del sistema de salud con NestJS y SQL Server. También refactoricé rutas legadas y participé en flujos de automatización de procesos, gestión documental y recolección de evidencias. Seguridad, control de acceso, trazabilidad y cumplimiento de LGPD orientaban cómo debía evolucionar cada ruta. Además del backend, colaboré en componentes compartidos del design system para alinear contratos de API con las interfaces usadas en la operación. ## Lo que me llevé El trabajo reforzó el cuidado necesario para evolucionar sistemas sensibles sin perder auditabilidad. En lugar de separar seguridad y entrega, traté acceso y trazabilidad como parte del contrato del producto. --- # Flapper URL: https://imrafaeldev.site/es/experiencias/flapper > Mi experiencia en Flapper con modernización incremental de un legado en producción. - [Inicio](/es/) - Flapper ## Flapper Mi experiencia en Flapper con modernización incremental de un legado en producción. Cargo Ingeniero de Software Full Stack Período sep/2021 a jun/2022 ## Contexto En Flapper, trabajé en una plataforma de aviación ejecutiva cuyo producto principal era un monolito PHP de más de siete años, con poca documentación útil y sin desarrolladores originales disponibles para explicar el sistema. ## Cómo trabajé El desafío no era reemplazar PHP por TypeScript. La aplicación estaba en producción y sostenía el negocio, así que empecé por descubrir el dominio. Usé la base de datos para mapear relaciones, identificar bounded contexts y planificar una migración incremental basada en el patrón Strangler. Migré módulos como personas, autenticación y aeronaves a servicios en Node.js, NestJS y Go. Para reducir dependencias relacionales entre contextos, trabajamos con proyecciones locales y eventos en Kafka; gRPC, REST y GraphQL se usaron según la necesidad de cada integración. ## Lo que me llevé Esta experiencia consolidó mi visión de modernización de legado: la tecnología viene después de entender fronteras, riesgos y una secuencia que preserve la operación. Además de entregar módulos, documenté decisiones y conduje workshops para que el equipo continuara la transformación. ## Caso relacionado El caso muestra cómo investigué el dominio a partir de la base de datos y el código y conduje la modernización incremental del monolito sin interrumpir la operación. [Leer el caso completo](/es/casos/modernizacion-monolito-sin-documentacion/) --- # Infosistemas URL: https://imrafaeldev.site/es/experiencias/infosistemas > Mi experiencia en Infosistemas con mensajería, integraciones y plataformas de movilidad. - [Inicio](/es/) - Infosistemas ## Infosistemas Mi experiencia en Infosistemas con mensajería, integraciones y plataformas de movilidad. Cargo Ingeniero de Software Senior / Arquitecto de Software Período feb/2025 a may/2026 ## Contexto En Infosistemas, trabajé en plataformas para arrendadoras, flotas, automotrices y operaciones de movilidad. Era un entorno enterprise de integraciones, flujos fiscales y servicios de alto volumen, donde trabajé junto a DevOps, SREs y DBAs. ## Cómo trabajé Mi trabajo combinó arquitectura y ejecución práctica. Rediseñé flujos entre microservicios, lideré integraciones en NestJS y Go e implementé trazabilidad de eventos críticos con NestJS y MongoDB. También evolucioné APIs, investigué problemas de seguridad y contribuí a jornadas digitales y componentes web cuando era necesaria la continuidad entre backend e interfaz. El principio que orientó mi trabajo fue hacer observables y tratables las fallas desde el diseño. En mensajería, traté durabilidad, retry, idempotencia y control de consumo como partes del flujo, no como correcciones posteriores. ## Lo que me llevé Esta experiencia amplió mi trabajo en sistemas con muchas dependencias y especialistas. Aprendí a convertir requisitos, riesgos y restricciones operativas en decisiones que siguieran claras hasta la validación con el cliente. ## Caso relacionado El caso detalla cómo rediseñé la mensajería RabbitMQ para hacer más previsibles los flujos críticos y reducir fallas intermitentes entre microservicios. [Leer el caso completo](/es/casos/mensajeria-rabbitmq/) --- # Maxmilhas URL: https://imrafaeldev.site/es/experiencias/maxmilhas > Mi experiencia en Maxmilhas con automatización posventa e integración de legado. - [Inicio](/es/) - Maxmilhas ## Maxmilhas Mi experiencia en Maxmilhas con automatización posventa e integración de legado. Cargo Ingeniero de Software Full Stack Período abr/2023 a oct/2023 ## Contexto En Maxmilhas, trabajé en un período corto e intenso, en flujos posventa de viajes relacionados con cancelaciones, cambios, cupones y comunicación con clientes. ## Cómo trabajé Desarrollé microservicios en Node.js, NestJS y Elixir para automatizar procesos que aún dependían de intervención del soporte. Implementé reglas de elegibilidad, expiración y acumulación de cupones, además de cálculos y validaciones para cancelaciones y cambios. En conjunto, esas automatizaciones redujeron en 34% la necesidad de intervención manual. Otro desafío fue integrar un monolito PHP 5.7, de más de diez años, al CRM comercial sin poner en riesgo el core de la operación. También evolucioné monitoreo de vuelos, notificaciones y mensajes proactivos. ## Lo que me llevé Aprendí a priorizar intervenciones pequeñas y reversibles cuando el resultado debía llegar rápido. En vez de proponer una transformación amplia, me concentré en puntos que liberaban trabajo operativo. --- # South System, QUIQ e Itaú URL: https://imrafaeldev.site/es/experiencias/south-system-quiq-itau > Mi experiencia con un marketplace white-label y multi-tenant para instituciones financieras. - [Inicio](/es/) - South System (asignado a QUIQ/Itaú) ## South System (asignado a QUIQ/Itaú) Mi experiencia con un marketplace white-label y multi-tenant para instituciones financieras. Cargo Ingeniero Backend Período jun/2022 a abr/2023 ## Contexto En South System, fui asignado a QUIQ para trabajar en un Marketplace as a Service para instituciones financieras. Itaú fue el primer contexto, pero el producto necesitaba incorporar nuevos bancos sin requerir un fork por cliente. ## Cómo trabajé Participé en arquitectura, modelado de datos, elección de tecnologías y refinamiento de reglas con producto. El resultado fue una plataforma white-label y multi-tenant, con aislamiento lógico entre tenants y Arquitectura Hexagonal para separar el dominio de las integraciones específicas. Trabajé con Node.js, TypeScript, MySQL, servicios asíncronos en Go y AWS. Estructuré pruebas unitarias y de integración para casos críticos y usé análisis estático como parte del flujo de calidad. ## Lo que me llevé Esta experiencia cambió cómo comunico arquitectura. Pasé a tratar la alineación con producto, Product Owner y stakeholders como parte de la decisión técnica, no como una etapa posterior al código. --- # Sustentec URL: https://imrafaeldev.site/es/experiencias/sustentec > Mi experiencia en Sustentec con sistemas de investigación, APIs y calidad. - [Inicio](/es/) - Sustentec ## Sustentec Mi experiencia en Sustentec con sistemas de investigación, APIs y calidad. Cargo Ingeniero de Software Full Stack Pleno Período ene/2021 a ago/2021 ## Contexto En Sustentec, trabajé en sistemas relacionados con laboratorios, investigación y desarrollo. La experiencia combinó el mantenimiento de un producto existente con la evolución de funcionalidades e integraciones. ## Cómo trabajé Desarrollé una API REST en Dart con Shelf para integrar bases de instituciones de investigación. También mantuve y evolucioné un sistema de gestión de laboratorios con Java, Spring Boot, JPA, Hibernate, PostgreSQL y Angular. Implementé pruebas de integración donde antes no existía esa cobertura, desarrollé informes y funcionalidades de extremo a extremo y participé en la recopilación de requisitos con clientes y el refinamiento de sprints con el Product Owner. ## Lo que me llevé Esta experiencia reforzó que la calidad no se limita a pruebas unitarias. En un sistema con varias capas, necesitaba validar el comportamiento real entre API, persistencia e interfaz. --- # VBET URL: https://imrafaeldev.site/es/experiencias/vbet > Mi experiencia en VBET con seguridad, analítica y rendimiento a escala. - [Inicio](/es/) - VBET ## VBET Mi experiencia en VBET con seguridad, analítica y rendimiento a escala. Cargo Ingeniero Backend Senior Período oct/2023 a feb/2025 ## Contexto En VBET, trabajé en un producto de analítica para afiliados e influencers de iGaming. El sistema calculaba métricas financieras y operativas en una plataforma que empezó a atender audiencias mucho mayores de lo previsto originalmente. ## Cómo trabajé Antes de abordar rendimiento, reduje riesgos de seguridad y mantenimiento en la API legada. Reemplacé consultas inseguras, organicé la base con Clean Architecture e inyección de dependencias y establecí pruebas y documentación para sostener los cambios siguientes. Después traté el rendimiento por etapas. Usé Go, goroutines, channels y consultas paralelas para reducir la primera etapa del cálculo de comisiones de cerca de siete a tres minutos. Como el SQL Server era externo y no podía cambiarse, diseñé un ETL con checkpoints, agregaciones precalculadas y reconciliación, diferenciando datos provisionales de datos consolidados. ## Lo que me llevé Esta experiencia consolidó cómo tomo decisiones de rendimiento: entender la restricción real, aceptar la consistencia adecuada para cada uso y después elegir la tecnología que la resuelve. ## Caso relacionado El caso profundiza en la evolución del dashboard de analítica: desde la reducción de riesgos en la API hasta ETL, precálculo, reconciliación y caché sobre un SQL Server externo. [Leer el caso completo](/es/casos/analitica-sql-server-externo/) --- # Rafael Pereira, ingeniero de software sénior URL: https://imrafaeldev.site/es > Portafolio institucional y hub editorial de Rafael Pereira. Trabajo en los puntos en los que los sistemas simples dejan de ser simples. Rafael Pereira / ingeniero de software / sistemas ## Trabajo en los puntos en los que los sistemas simples *dejan de ser simples*. Estudios de caso, proyectos y textos técnicos sobre escala, fallos, legado y reglas de negocio. Cada pieza muestra la restricción, la decisión y hasta dónde llega la solución. [Ver los estudios de caso](/es/casos/) [Ver opciones de contacto](/es/contacto/) [Case en foco **Infosistemas** ~98% · reducción de fallos intermitentes en los flujos críticos Abrir el case](/es/casos/mensajeria-rabbitmq/) [**VBET** ~7 min → <1 s](/es/casos/analitica-sql-server-externo/) - [~98% Infosistemas: reducción de fallos intermitentes en los flujos críticos](/es/casos/mensajeria-rabbitmq/) - [~7 min → <1 s VBET: comisiones, de la carga original a la caché caliente](/es/casos/analitica-sql-server-externo/) - [~25% Flapper: menos tablas en la separación por dominios](/es/casos/modernizacion-monolito-sin-documentacion/) Señal Método ## La restricción viene antes del diagrama. Empiezo por lo que ocurre cuando un mensaje falla. La base de datos no puede cambiar. El legado no puede parar. El camino feliz viene después. La decisión registra la alternativa descartada y la condición que justificaría revisarla. El código muestra qué se hizo; el case muestra por qué. Prueba ## Tres sistemas, tres restricciones Infosistemas feb/2025 – may/2026 ~98% reducción de fallos intermitentes en los flujos críticos ### [Infosistemas: contrato de fallo en la mensajería RabbitMQ](/es/casos/mensajeria-rabbitmq/) Fallos intermitentes entre microservicios sin contrato para retry, DLQ o duplicidad. Más consumidores solo desplazaban la sobrecarga. [Abrir el case](/es/casos/mensajeria-rabbitmq/) Contrato de falha na mensageria Publicação confirmada, consumo com prefetch controlado, retry com backoff e DLQ por fluxo. Escalar só consumidores fica de fora do desenho. [Ver o caso](/es/casos/mensajeria-rabbitmq/) VBET oct/2023 – feb/2025 ~7 min → <1 s comisiones, de la carga original a la caché caliente ### [VBET: analítica sobre un SQL Server que no podíamos cambiar](/es/casos/analitica-sql-server-externo/) La base era de otro equipo. El dashboard necesitaba dejar de depender de un schema que no controlábamos. [Abrir el case](/es/casos/analitica-sql-server-externo/) Pipeline de comissões Cada estágio corresponde a uma decisão incremental documentada no case. Números de outras histórias não entram neste desenho. [Ver o caso](/es/casos/analitica-sql-server-externo/) Flapper sep/2021 – jun/2022 ~25% menos tablas en la separación por dominios ### [Flapper: descubrir el dominio antes de separar el monolito](/es/casos/modernizacion-monolito-sin-documentacion/) El producto no podía parar, y los autores originales ya no estaban. Antes de migrar, fue preciso descubrir qué fronteras aún revelaba la base. [Abrir el case](/es/casos/modernizacion-monolito-sin-documentacion/) Descoberta de domínio antes da migração O banco legado revela fronteiras; pessoas, autenticação e aeronaves saem gradualmente para contextos com persistência própria. [Ver o caso](/es/casos/modernizacion-monolito-sin-documentacion/) Artefactos ## Proyectos seleccionados Repositorio público ### [SMS Manager](/es/proyectos/sms-manager/) Importar CSV no puede bloquear la API Nest. La campaña persiste y publica en la cola RabbitMQ; consumidores en TypeScript, Go o Rust graban el resultado en Mongo. La API no espera a la operadora. [Abrir el proyecto](/es/proyectos/sms-manager/) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. Experimento documentado ### [goc_mcp](/es/proyectos/goc-mcp/) Maestro (Codex/Cursor) delega vía MCP; daemon FIFO y workers OpenCode ejecutan con estado en SQLite. La orquestación funcionó, pero la medición no confirmó reducción de costo o tiempo. [Abrir el proyecto](/es/proyectos/goc-mcp/) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. Producto propio, desktop ### [md2cv](/es/proyectos/md2cv/) Perfil y versiones inmutables quedan en el SQLite de la máquina. El agente supervisado solo propone cambios bajo schema; ATS y exportación en PDF/DOCX reutilizan el mismo grafo, sin backend SaaS dueño de los datos. [Abrir el proyecto](/es/proyectos/md2cv/) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. [Ver todos los proyectos](/es/proyectos/) Recorrido ## Una trayectoria de sistemas, no de cargos - 2025 a 2026 ### Arquitectura Plataformas de alquiler y flotas. La prueba pública de esta etapa es la mensajería. Infosistemas - 2023 a 2025 ### Analytics sobre una base externa SQL Server de otro equipo. El dashboard tenía que funcionar sin depender de un schema que no controlábamos. VBET - 2023 ### Postventa sobre legado El core en PHP 5.7 sostenía la operación. La automatización liberó soporte sin reescribir el monolito. Maxmilhas - 2022 a 2023 ### Marketplace multi-tenant White-label para incorporar bancos sin fork por cliente. South System / QUIQ-Itaú - 2021 a 2022 ### Modernización incremental El monolito no tenía a sus autores originales. La migración empezó con lo que la base de datos todavía dejaba leer. Flapper Práctica ## Cómo avanza el trabajo Cada etapa limita la siguiente. Sin esas restricciones, la arquitectura se vuelve gusto personal. Cómo avanza el trabajo - Contexto - Restricción - Decisión - Evidencia - Límite 01 ### Contexto Quién sufre cuando esto se rompe, y en qué operación. 02 ### Restricción Lo que no puede parar, cambiar o tratarse como capacidad extra. 03 ### Decisión El camino elegido, con la alternativa descartada a la vista. 04 ### Evidencia Qué se midió, en qué escenario, con qué autoría. 05 ### Límite La condición que justificaría revisar la decisión. Contacto ## ¿Tienes un sistema que dejó de ser simple? Escríbeme directo por @imrafaeldev, sin formulario — para conversación profesional, empieza en LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/) [YouTube](https://www.youtube.com/@imrafaeldev) [GitHub](https://github.com/imrafaeldev) [LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Abrir la página de contacto](/es/contacto/) --- # DiffVision URL: https://imrafaeldev.site/es/proyectos/diffvision > CLI npm local-first para revisar diffs Git, con UI local, comentarios en el repositorio, exportación en Markdown/JSON y servidor MCP. La revisión visual por IA permanece mock. - [Inicio](/es/) - [Proyectos](/es/proyectos/) - DiffVision CLI pública; revisión por IA aún mock ## DiffVision El diff Git abre en la UI local; comentarios y exportación en Markdown quedan en el repositorio. La revisión visual por IA permanece mock. [Repositorio](https://github.com/imrafaeldev/diffvision-app) Revisão local-first Diff no disco, UI local, comentários e export em `.diffvision/`. A plataforma remota fica de fora; a revisão por IA visual ainda é mock. - [01Problema](#problema) - [02Restricciones](#restricciones) - [03Decisión](#decision) - [04Estado actual](#estado-actual) - [05Limitaciones](#limitaciones) ## Problema Revisar un diff Git en herramienta SaaS envía código fuera y mezcla UI remota con el historial local. La revisión necesita funcionar offline, con hunks, filtros, bookmarks y comentarios anclados en líneas. ## Restricciones Preferencias e informes deben vivir en el propio repositorio (.diffvision/). La CLI npm inicia backend y UI locales. La integración con asistente no puede venderse como lista si aún es prototipo. ## Decisión La CLI inspecciona el Git, interpreta diff unificado y sube interfaz web. Backend Fastify con snapshot y WebSocket; UI React/Vite. Exportación Markdown/JSON en el repositorio. Paquete diffvision-mcp por stdio para resumir el repositorio, leer patches y registrar comentarios. El asistente visual de revisión por IA se declara mock/prototipo; la escritura de comentarios vía MCP es funcional. ## Estado actual Distribuido como CLI npm, con ejecución local-first. ## Limitaciones No sustituye el flujo de review de GitHub. El flujo de IA visual no debe leerse como producto acabado. --- # goc_mcp URL: https://imrafaeldev.site/es/proyectos/goc-mcp > Orquestación local de agentes vía MCP en Go: maestro, daemon FIFO, workers OpenCode y SQLite. La entrega funcionó, pero la hipótesis de costo y tiempo no se confirmó en esta medición. - [Inicio](/es/) - [Proyectos](/es/proyectos/) - goc_mcp Experimento documentado ## goc_mcp Maestro (Codex/Cursor) delega vía MCP; daemon FIFO y workers OpenCode ejecutan con estado en SQLite. La orquestación funcionó, pero la medición no confirmó reducción de costo o tiempo. [Repositorio](https://github.com/imrafaeldev/goc_mcp) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. - [01Problema](#problema) - [02Restricciones](#restricciones) - [03Decisión](#decision) - [04Estado actual](#estado-actual) - [05Limitaciones](#limitaciones) ## Problema Un maestro (Codex o Cursor) necesita delegar trabajo a workers OpenCode con ciclo de vida explícito: planear, iniciar, acompañar, responder, cancelar y recuperar, sin recursión infinita de agentes. ## Restricciones Todo es local. El daemon autentica en loopback. Hay límite global y por workspace. Las tareas interrumpidas necesitan reconciliarse. La corrupción de estado no puede volverse escritura ciega. En el camino feliz: un worker OpenCode por vez, sin descomposición automática que deje al maestro sin control. ## Decisión Implementación en Go: gateways MCP por stdio, daemon único, máquina de estados y ejecutor FIFO. Persistencia en SQLite con WAL; artefactos en JSONL. Adaptador OpenCode con servidor como camino principal y CLI como fallback. Aislamiento contra delegación recursiva y diagnóstico solo lectura si el estado se corrompe. ## Estado actual Repositorio con pruebas, ADRs y benchmarks. La orquestación funcionó. Las mediciones publicadas en el propio proyecto no confirmaron la hipótesis de reducir tiempo y costo. ## Limitaciones Es un experimento. No afirma ganancia de productividad de ingeniería de agentes en producción. El resultado negativo de la hipótesis forma parte del artefacto. --- # Proyectos URL: https://imrafaeldev.site/es/proyectos > Artefactos con problema, restricciones y estado actual del repositorio. - [Inicio](/es/) - Proyectos ## Proyectos Artefactos con problema, restricciones y estado actual del repositorio. Repositorio público ## [SMS Manager](/es/proyectos/sms-manager/) Importar CSV no puede bloquear la API Nest. La campaña persiste y publica en la cola RabbitMQ; consumidores en TypeScript, Go o Rust graban el resultado en Mongo. La API no espera a la operadora. [Abrir el proyecto](/es/proyectos/sms-manager/) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. Experimento documentado ## [goc_mcp](/es/proyectos/goc-mcp/) Maestro (Codex/Cursor) delega vía MCP; daemon FIFO y workers OpenCode ejecutan con estado en SQLite. La orquestación funcionó, pero la medición no confirmó reducción de costo o tiempo. [Abrir el proyecto](/es/proyectos/goc-mcp/) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. Producto propio, desktop ## [md2cv](/es/proyectos/md2cv/) Perfil y versiones inmutables quedan en el SQLite de la máquina. El agente supervisado solo propone cambios bajo schema; ATS y exportación en PDF/DOCX reutilizan el mismo grafo, sin backend SaaS dueño de los datos. [Abrir el proyecto](/es/proyectos/md2cv/) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. CLI pública; revisión por IA aún mock ## [DiffVision](/es/proyectos/diffvision/) El diff Git abre en la UI local; comentarios y exportación en Markdown quedan en el repositorio. La revisión visual por IA permanece mock. [Abrir el proyecto](/es/proyectos/diffvision/) Revisão local-first Diff no disco, UI local, comentários e export em `.diffvision/`. A plataforma remota fica de fora; a revisão por IA visual ainda é mock. Estación de trabajo editorial ## [Post Engine](/es/proyectos/post-engine/) La entrevista adaptativa extrae evidencias; el gateway híbrido (LLM + heurística) bloquea vivencia inventada. Solo el contenido confirmado sigue a borrador y exportación en Markdown o SlideMark. [Abrir el proyecto](/es/proyectos/post-engine/) Autoria antes da geração Entrevista extrai evidência; briefing e storyboard preparam o material; o gateway veta fabricado. Só o confirmado segue para rascunho e export. ## Otros trabajos, otros contextos Proyectos visuales para explorar manualmente. - [Gran Goiás Sitio institucional de Gran Goiás, marmolería con ejecución en piedra para obras de escala. Visitar sitio](https://gran-goias.vercel.app/) - [Gabriel | Nutrición Deportiva Sitio institucional de Gabriel Pereira para nutrición deportiva, con contenido real y estr](https://gabriel-pereira-nutri.vercel.app/) --- # md2cv URL: https://imrafaeldev.site/es/proyectos/md2cv > Estudio desktop local-first para perfil profesional, currículos Markdown, versiones inmutables, ATS y adaptación a vacantes con agentes supervisados. Los datos quedan en el SQLite de la máquina. - [Inicio](/es/) - [Proyectos](/es/proyectos/) - md2cv Producto propio, desktop ## md2cv Perfil y versiones inmutables quedan en el SQLite de la máquina. El agente supervisado solo propone cambios bajo schema; ATS y exportación en PDF/DOCX reutilizan el mismo grafo, sin backend SaaS dueño de los datos. [Repositorio](https://github.com/imrafaeldev/md2cv) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. - [01Problema](#problema) - [02Restricciones](#restricciones) - [03Decisión](#decision) - [04Estado actual](#estado-actual) - [05Limitaciones](#limitaciones) ## Problema Los perfiles profesionales se esparcen entre docs, LinkedIn y exports. Cada candidatura pide un ángulo distinto. Adaptar el currículo con un LLM sin frontera inventa experiencia y borra el contexto de la versión anterior. Se necesita un grafo versionado en la máquina que pregunte lo que falta y rechace salida incompleta, sin prometer contratación ni aprobación automática por ATS. ## Restricciones Desktop y local-first (Electron): sin cuenta propietaria y sin backend dueño de los datos. - IPC tipado y validado entre renderer y proceso principal. - SQLite con foreign keys, WAL, migraciones con checksum, verificaciones de integridad y backup antes de operación destructiva. - Agentes (Codex, Cursor, OpenCode) entran por adaptadores aislados, no como dueños de la base. - La adaptación a una candidatura no graba en el perfil sin confirmación explícita. - Acceso externo solo bajo acción del usuario (búsqueda de URL de empresa o CLI de IA ya configurada en el equipo); el producto no almacena credenciales de proveedores. ## Decisión Renderer React/Vite separado del proceso principal. El producto organiza el trabajo en un único grafo local: - Perfil: experiencias, formación, cursos, idiomas, proyectos, enlaces, habilidades y empresas por persona. - Currículos: Markdown, versiones inmutables, restauración rastreable y panorama de evolución. - ATS: diagnósticos estructurales y puntuación orientativa; PDF textual marcado y DOCX semántico. - Candidaturas: empresa, vacante, currículo base, versión y estado en el mismo contexto. - Agentes: máquina de estados para preguntas, intentos y propuesta de nueva versión bajo schema; solo persiste con confirmación. - Datos: importación y exportación versionada del grafo completo; compilador Markdown (unified/remark) compartido por preview, auditoría y export. Flujo canónico: perfil → currículo base → versión inmutable → auditoría ATS → PDF/DOCX. La rama de candidatura pasa por agente local supervisado antes del currículo adaptado. ## Estado actual Repositorio público bajo MIT, portal de documentación y releases x64 para Linux (AppImage, .deb, .rpm) y Windows (NSIS y portable), con CI y pruebas Vitest y Playwright en persistencia, agentes, ATS y flujos Electron. El export profesional usado en este sitio nace de ese producto y no es leído por el sitio en runtime. ## Limitaciones No sustituye la revisión humana ni promete contratación o aprobación automática por plataformas de reclutamiento. La adaptación a vacantes depende de respuestas confirmadas; el sistema se niega a fabricar experiencia. Los binarios Windows de esta fase no poseen firma de código. El proyecto es autoral y sin finalidad comercial del mantenedor; la licencia MIT permite reutilización en sus términos. --- # Post Engine URL: https://imrafaeldev.site/es/proyectos/post-engine > Estación editorial centrada en autoría: entrevista, briefing, storyboard y exportación después de que el gateway bloquee contenido fabricado. Prompts versionados; workspace LLM aislado. - [Inicio](/es/) - [Proyectos](/es/proyectos/) - Post Engine Estación de trabajo editorial ## Post Engine La entrevista adaptativa extrae evidencias; el gateway híbrido (LLM + heurística) bloquea vivencia inventada. Solo el contenido confirmado sigue a borrador y exportación en Markdown o SlideMark. [Repositorio](https://github.com/imrafaeldev/post-engine) Autoria antes da geração Entrevista extrai evidência; briefing e storyboard preparam o material; o gateway veta fabricado. Só o confirmado segue para rascunho e export. - [01Problema](#problema) - [02Restricciones](#restricciones) - [03Decisión](#decision) - [04Estado actual](#estado-actual) - [05Limitaciones](#limitaciones) ## Problema Generar un post “profesional” con LLM a partir de un eslogan inventa biografía. El flujo necesita entrevistar, identificar lagunas, preparar el briefing, redactar y exportar, y rechazar lo no confirmado. ## Restricciones Núcleo en Python con fronteras entre entrevista, generación, preservación de autoría, segmentación y persistencia. Llamadas a modelo en workspace aislado, allowlist de proveedor y prompts como contratos versionados. Evaluación híbrida: LLM más heurísticas deterministas. La ausencia de experiencia nunca se vuelve falsa vivencia. ## Decisión Entrevistas adaptativas, briefing autoral, storyboard, veto a contenido fabricado, registry SQLite de prompts con rollback, interfaz textual y frontend React/Vite para revisar fases. Exportación Markdown o SlideMark JSON tras la evaluación. ## Estado actual Base con pruebas de entrevista, aislamiento de LLM, registry, persistencia y conversión SlideMark. ## Limitaciones No es un generador genérico de thought leadership. Sin repertorio real, el sistema se niega; no completa la biografía. --- # SMS Manager URL: https://imrafaeldev.site/es/proyectos/sms-manager > Campañas de SMS desacopladas: la API Nest persiste y publica, la cola entrega y consumidores en TypeScript, Go o Rust graban el resultado. Token opaco, Redis y gRPC entre servicios. - [Inicio](/es/) - [Proyectos](/es/proyectos/) - SMS Manager Repositorio público ## SMS Manager Importar CSV no puede bloquear la API Nest. La campaña persiste y publica en la cola RabbitMQ; consumidores en TypeScript, Go o Rust graban el resultado en Mongo. La API no espera a la operadora. [Repositorio](https://github.com/imrafaeldev/sms-manager) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. - [01Problema](#problema) - [02Restricciones](#restricciones) - [03Decisión](#decision) - [04Estado actual](#estado-actual) - [05Limitaciones](#limitaciones) ## Problema Las campañas de SMS parten de archivos CSV, usuarios, empresas y autenticación entre servicios. Validar y persistir en el mismo proceso que dispara miles de mensajes acopla la API al ritmo de la operadora y de la cola. ## Restricciones El entorno debe ser reproducible. La autenticación entre servicios no puede depender de JWT opaco sin revocación. Los consumidores en más de un lenguaje existen para comparar el mismo contrato de mensajería. ## Decisión Siete aplicaciones: APIs NestJS de usuarios/autenticación y de empresas/campañas; consumidores equivalentes en Node.js/TypeScript, Go y Rust; aprovisionador declarativo de exchanges, colas y bindings; generador de masa CSV. PostgreSQL/TypeORM para datos relacionales en la API; MongoDB para el resultado del consumo; Redis para caché de token opaco (revocable); gRPC para autenticación entre servicios; RabbitMQ con exchanges topic para los lotes. La API publica y sigue sin bloquearse al ritmo de la operadora. ## Estado actual Repositorio público con Docker Compose para PostgreSQL, MongoDB, Redis y RabbitMQ. La arquitectura separa dominio, aplicación e infraestructura en los contextos de usuarios, autenticación y empresas; los tres consumidores implementan el mismo contrato de mensaje. ## Limitaciones Es un artefacto de estudio y operación local de mensajería, no un producto comercial con SLA de operadora. Los tres consumidores demuestran el contrato; no afirman que los tres lenguajes corran juntos en producción del autor. --- # Azify URL: https://imrafaeldev.site/experiencias/azify > Minha consultoria na Azify com liquidação, BaaS e serviços financeiros. - [Início](/) - Azify ## Azify Minha consultoria na Azify com liquidação, BaaS e serviços financeiros. Cargo Engenheiro Backend Sênior — Consultoria Período mar/2025 a jun/2025 ## O contexto Na Azify, atuei como consultor em infraestrutura financeira para fintechs e pequenos bancos. O produto reunia capacidades como Pix, transferências, cartões, carteira digital e outros serviços bancários. ## Como atuei Participei de decisões arquiteturais e ajudei a estruturar práticas de desenvolvimento para um ambiente em que consistência, segurança e estabilidade tinham impacto financeiro direto. Desenvolvi um motor de liquidação em NestJS com integrações a múltiplas exchanges, monitoramento de risco e controles de compliance. Também trabalhei na integração de exchanges e blockchains aos fluxos transacionais e na evolução de uma plataforma BaaS multi-tenant com OAuth 2.0, JWT e criptografia. Com profiling de consultas, revisão de índices e ajuste do uso de Redis, reduzi em 30% a latência de APIs financeiras críticas. ## O que levo Essa consultoria retomou temas recorrentes da minha trajetória, como pagamentos, multi-tenancy e sistemas transacionais, com um grau de responsabilidade ainda maior sobre autorização e consistência. --- # Braistech URL: https://imrafaeldev.site/experiencias/braistech > Minha experiência na Braistech com produto, microsserviços e criptoativos. - [Início](/) - Braistech ## Braistech Minha experiência na Braistech com produto, microsserviços e criptoativos. Cargo Engenheiro de Software Full Stack Pleno Período nov/2019 a jan/2021 ## O contexto Na Braistech, vivi uma das minhas primeiras experiências de produto em um ambiente pequeno, com pouca gente e responsabilidade distribuída. O domínio envolvia contratos de criptoativos e movimentações financeiras. ## Como atuei Liderei a estruturação do sistema principal com Node.js e NestJS, participei do desenho de microsserviços para o núcleo do negócio e desenvolvi aplicações em Flutter. Também construí um sistema de contratos e trabalhei com integrações de pagamento relacionadas ao ecossistema da Binance. Participei ainda da transição de uma organização baseada em MVC para uma arquitetura mais próxima de Clean Architecture. O objetivo era reduzir acoplamento e facilitar a manutenção de um sistema em crescimento, enquanto eu orientava desenvolvedores juniores nas decisões do código. ## O que levo Essa etapa consolidou meu interesse por backend e arquitetura. O contato com produto completo também me deu uma visão full stack que segue útil nas conversas com frontend e produto. --- # EDS e Polícia Civil do Rio de Janeiro URL: https://imrafaeldev.site/experiencias/eds-policia-civil-rio > Minha experiência de consultoria em sistemas públicos sensíveis. - [Início](/) - EDS (Polícia Civil do Rio de Janeiro) ## EDS (Polícia Civil do Rio de Janeiro) Minha experiência de consultoria em sistemas públicos sensíveis. Cargo Engenheiro Backend — Consultoria Período jul/2025 a dez/2025 ## O contexto Na consultoria para a EDS, trabalhei em sistemas destinados à Polícia Civil do Rio de Janeiro. O contexto envolvia uma operação pública crítica, um sistema de gestão de saúde de alto volume e a evolução de um ERP jurídico. ## Como atuei Estruturei o backend do sistema de saúde com NestJS e SQL Server. Também refatorei rotas legadas e participei do desenho de fluxos para automação de processos, gestão documental e coleta de evidências. Segurança, controle de acesso, rastreabilidade e LGPD não eram requisitos isolados: orientavam como cada rota precisava evoluir. Além do backend, colaborei com a manutenção de componentes compartilhados do design system para alinhar contratos de API e o comportamento das interfaces usadas na operação. ## O que levo Foi uma experiência que reforçou o cuidado necessário para evoluir sistemas sensíveis sem perder auditabilidade. Em vez de separar segurança da entrega, tratei acesso e rastreabilidade como parte do contrato do produto. --- # Flapper URL: https://imrafaeldev.site/experiencias/flapper > Minha experiência na Flapper com modernização incremental de um legado em produção. - [Início](/) - Flapper ## Flapper Minha experiência na Flapper com modernização incremental de um legado em produção. Cargo Engenheiro de Software Full Stack Período set/2021 a jun/2022 ## O contexto Na Flapper, trabalhei em uma plataforma de aviação executiva cujo produto principal era um monólito PHP com mais de sete anos, pouca documentação útil e sem os desenvolvedores originais disponíveis para explicar o sistema. ## Como atuei O desafio não era trocar PHP por TypeScript. A aplicação estava em produção e sustentava o negócio, então comecei pela descoberta do domínio. Usei o banco de dados para mapear relações, identificar bounded contexts e planejar uma migração incremental baseada no padrão Strangler. Migrei módulos como pessoas, autenticação e aeronaves para serviços em Node.js, NestJS e Go. Para reduzir dependências relacionais entre contextos, trabalhamos com projeções locais e eventos no Kafka; gRPC, REST e GraphQL foram aplicados conforme a necessidade de cada integração. ## O que levo Foi uma experiência que consolidou minha visão de modernização de legado: tecnologia vem depois de entender fronteiras, riscos e uma sequência que preserve a operação. Além de entregar módulos, documentei decisões e conduzi workshops para que o time pudesse continuar a transformação. ## Case relacionado O case mostra como investiguei o domínio a partir de banco e código e conduzi a modernização incremental do monólito sem interromper a operação. [Ler case completo](/casos/modernizacao-monolito-sem-documentacao/) --- # Infosistemas URL: https://imrafaeldev.site/experiencias/infosistemas > Minha experiência na Infosistemas com mensageria, integrações e plataformas de mobilidade. - [Início](/) - Infosistemas ## Infosistemas Minha experiência na Infosistemas com mensageria, integrações e plataformas de mobilidade. Cargo Engenheiro de Software Sênior / Arquiteto de Software Período fev/2025 a mai/2026 ## O contexto Na Infosistemas, atuei em plataformas para locadoras, frotas, montadoras e operações de mobilidade. Era um ambiente enterprise, com integrações, fluxos fiscais e serviços de grande volume, no qual trabalhei próximo de DevOps, SREs e DBAs. ## Como atuei Minha atuação combinou arquitetura e execução hands-on. Redesignei fluxos entre microsserviços, liderei integrações em NestJS e Go e implementei rastreabilidade de eventos críticos com NestJS e MongoDB. Também evoluí APIs, investiguei problemas de segurança e participei de jornadas digitais e de componentes do webapp quando a continuidade entre backend e interface era necessária. O ponto que mais orientou meu trabalho foi tornar falhas observáveis e tratáveis desde o desenho. Na mensageria, tratei durabilidade, retry, idempotência e controle de consumo como partes do fluxo, não como correções posteriores. ## O que levo Essa experiência ampliou minha atuação em sistemas com muitas dependências e especialistas envolvidos. Aprendi a transformar requisitos, riscos e restrições operacionais em decisões que continuassem claras até a validação com o cliente. ## Case relacionado O case detalha como redesenhei a mensageria RabbitMQ para tornar fluxos críticos mais previsíveis e reduzir falhas intermitentes entre microsserviços. [Ler case completo](/casos/mensageria-rabbitmq/) --- # Maxmilhas URL: https://imrafaeldev.site/experiencias/maxmilhas > Minha experiência na Maxmilhas com automação de pós-venda e legado. - [Início](/) - Maxmilhas ## Maxmilhas Minha experiência na Maxmilhas com automação de pós-venda e legado. Cargo Engenheiro de Software Full Stack Período abr/2023 a out/2023 ## O contexto Na Maxmilhas, trabalhei em um período curto e intenso, em fluxos de pós-venda ligados a cancelamentos, remarcações, cupons e comunicação com clientes. ## Como atuei Desenvolvi microsserviços em Node.js, NestJS e Elixir para automatizar processos que ainda dependiam do suporte. Implementei regras de elegibilidade, expiração e cumulatividade de cupons, além de cálculos e validações necessários aos fluxos de cancelamento e remarcação. O conjunto dessas automações reduziu em 34% a necessidade de intervenção manual. Outro desafio foi integrar um monólito PHP 5.7, com mais de dez anos, ao CRM comercial sem colocar em risco o core da operação. Também evoluí monitoramento de voos, notificações e mensagens proativas. ## O que levo Aprendi a priorizar intervenções pequenas e reversíveis quando o resultado precisava aparecer rápido. Em vez de propor uma transformação ampla, concentrei a mudança nos pontos que liberavam trabalho operacional. --- # South System, QUIQ e Itaú URL: https://imrafaeldev.site/experiencias/south-system-quiq-itau > Minha experiência com marketplace white-label e multi-tenant para instituições financeiras. - [Início](/) - South System (alocado na QUIQ/Itaú) ## South System (alocado na QUIQ/Itaú) Minha experiência com marketplace white-label e multi-tenant para instituições financeiras. Cargo Engenheiro Backend Período jun/2022 a abr/2023 ## O contexto Na South System, fui alocado na QUIQ para trabalhar em um Marketplace as a Service voltado a instituições financeiras. O primeiro contexto era o Itaú, mas o produto precisava receber novos bancos sem exigir um fork por cliente. ## Como atuei Participei do desenho da arquitetura, da modelagem de banco, da escolha de tecnologias e do refinamento das regras com produto. A solução foi construída como uma plataforma white-label e multi-tenant, com isolamento lógico entre tenants e Arquitetura Hexagonal para manter o domínio separado das integrações específicas. Trabalhei com Node.js, TypeScript, MySQL, serviços assíncronos em Go e AWS. Estruturei testes unitários e de integração para os casos críticos e usei análise estática como parte do fluxo de qualidade. ## O que levo Essa experiência mudou minha forma de comunicar arquitetura. Passei a tratar alinhamento com produto, PO e stakeholders como parte da decisão técnica, não como uma etapa posterior ao código. --- # Sustentec URL: https://imrafaeldev.site/experiencias/sustentec > Minha experiência na Sustentec com sistemas de pesquisa, APIs e qualidade. - [Início](/) - Sustentec ## Sustentec Minha experiência na Sustentec com sistemas de pesquisa, APIs e qualidade. Cargo Engenheiro de Software Full Stack Pleno Período jan/2021 a ago/2021 ## O contexto Na Sustentec, trabalhei em sistemas ligados a laboratórios, pesquisa e desenvolvimento. A experiência combinou a manutenção de um produto existente com a evolução de funcionalidades e integrações. ## Como atuei Desenvolvi uma API REST em Dart com Shelf para integrar bases de instituições de pesquisa. Também mantive e evoluí um sistema de gestão de laboratórios com Java, Spring Boot, JPA, Hibernate, PostgreSQL e Angular. Implementei testes de integração onde antes não havia essa cobertura, desenvolvi relatórios e entregas ponta a ponta e participei da coleta de requisitos com clientes e do refinamento de sprints com o Product Owner. ## O que levo Essa experiência reforçou que qualidade não se limita a testes unitários. Em sistemas com várias camadas, eu precisava validar o comportamento real entre API, persistência e interface. --- # VBET URL: https://imrafaeldev.site/experiencias/vbet > Minha experiência na VBET com segurança, analytics e performance em escala. - [Início](/) - VBET ## VBET Minha experiência na VBET com segurança, analytics e performance em escala. Cargo Engenheiro Backend Sênior Período out/2023 a fev/2025 ## O contexto Na VBET, trabalhei em um produto de analytics para afiliados e influenciadores de iGaming. O sistema calculava métricas financeiras e operacionais em uma plataforma que passou a atender bases de usuários muito maiores do que as previstas originalmente. ## Como atuei Antes de atacar desempenho, comecei reduzindo riscos de segurança e manutenção na API legada. Substituí consultas inseguras, organizei a base com Clean Architecture e injeção de dependências e estabeleci testes e documentação para sustentar as próximas mudanças. Depois, tratei a performance em etapas. Usei Go, goroutines, channels e consultas paralelas para reduzir a primeira etapa do cálculo de comissões de cerca de sete para três minutos. Como o SQL Server era externo e não podia ser alterado, desenhei um ETL com checkpoints, agregações pré-calculadas e reconciliação, distinguindo dados provisórios de dados consolidados. ## O que levo Essa experiência consolidou a forma como tomo decisões de performance: entender o limite real, aceitar a consistência compatível com cada uso e só então escolher a tecnologia que resolve a restrição. ## Case relacionado O case aprofunda a evolução do dashboard de analytics: da correção de riscos na API à estratégia de ETL, pré-cálculo, reconciliação e cache sobre um SQL Server externo. [Ler case completo](/casos/analytics-sql-server-externo/) --- # Verificação da fundação URL: https://imrafaeldev.site/fundacao > Página temporária, sem índice, usada para validar a troca de idioma quando não há variante publicada. ## Página sem variantes em inglês ou espanhol O seletor de idioma deve levar à home do idioma escolhido, não a um fallback em português. Esta rota existe só para verificar a fundação multilíngue. Não faz parte da navegação pública nem do sitemap. --- # /google59795d012238450d URL: https://imrafaeldev.site/google59795d012238450d google-site-verification: google59795d012238450d.html --- # Rafael Pereira, engenheiro de software sênior URL: https://imrafaeldev.site > Portfólio institucional e hub editorial de Rafael Pereira. Trabalho nos pontos em que sistemas simples deixam de ser simples. Rafael Pereira / engenheiro de software / sistemas ## Eu trabalho nos pontos em que sistemas simples *deixam de ser simples*. Estudos de caso, projetos e textos técnicos sobre escala, falhas, legado e regras de negócio. Cada peça mostra a restrição, a decisão e até onde a solução funciona. [Ver os estudos de caso](/casos/) [Ver formas de contato](/contato/) [Case em foco **Infosistemas** ~98% · redução de falhas intermitentes nos fluxos críticos Abrir o case](/casos/mensageria-rabbitmq/) [**VBET** ~7 min → <1 s](/casos/analytics-sql-server-externo/) - [~98% Infosistemas: redução de falhas intermitentes nos fluxos críticos](/casos/mensageria-rabbitmq/) - [~7 min → <1 s VBET: comissões, da carga original ao cache quente](/casos/analytics-sql-server-externo/) - [~25% Flapper: menos tabelas na separação por domínios](/casos/modernizacao-monolito-sem-documentacao/) Sinal Método ## A restrição vem antes do diagrama. Começo pelo que acontece quando uma mensagem falha. O banco não pode mudar. O legado não pode parar. O caminho feliz vem depois. A decisão registra a alternativa descartada e a condição que justificaria revê-la. O código mostra o que foi feito; o case mostra por quê. Prova ## Três sistemas, três restrições Infosistemas fev/2025 a mai/2026 ~98% redução de falhas intermitentes nos fluxos críticos ### [Infosistemas: contrato de falha na mensageria RabbitMQ](/casos/mensageria-rabbitmq/) Falhas intermitentes entre microsserviços sem contrato para retry, DLQ ou duplicidade. Mais consumidores só empurravam a sobrecarga. [Abrir o case](/casos/mensageria-rabbitmq/) Contrato de falha na mensageria Publicação confirmada, consumo com prefetch controlado, retry com backoff e DLQ por fluxo. Escalar só consumidores fica de fora do desenho. [Ver o caso](/casos/mensageria-rabbitmq/) VBET out/2023 a fev/2025 ~7 min → <1 s comissões, da carga original ao cache quente ### [VBET: analytics sobre um SQL Server que não podíamos mudar](/casos/analytics-sql-server-externo/) O banco era de outro time. O dashboard precisava deixar de depender de um schema que não controlávamos. [Abrir o case](/casos/analytics-sql-server-externo/) Pipeline de comissões Cada estágio corresponde a uma decisão incremental documentada no case. Números de outras histórias não entram neste desenho. [Ver o caso](/casos/analytics-sql-server-externo/) Flapper set/2021 a jun/2022 ~25% menos tabelas na separação por domínios ### [Flapper: descobrir o domínio antes de separar o monólito](/casos/modernizacao-monolito-sem-documentacao/) O produto não podia parar, e os autores originais já não estavam lá. Antes de migrar, foi preciso descobrir quais fronteiras o banco ainda revelava. [Abrir o case](/casos/modernizacao-monolito-sem-documentacao/) Descoberta de domínio antes da migração O banco legado revela fronteiras; pessoas, autenticação e aeronaves saem gradualmente para contextos com persistência própria. [Ver o caso](/casos/modernizacao-monolito-sem-documentacao/) Artefatos ## Projetos selecionados Repositório público ### [SMS Manager](/projetos/sms-manager/) Importar CSV não pode travar a API Nest. A campanha persiste e publica na fila RabbitMQ; consumidores em TypeScript, Go ou Rust gravam o resultado no Mongo. A API não espera a operadora. [Abrir o projeto](/projetos/sms-manager/) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. Experimento documentado ### [goc_mcp](/projetos/goc-mcp/) Maestro (Codex/Cursor) delega via MCP; daemon FIFO e workers OpenCode executam com estado em SQLite. A orquestração funcionou, mas a medição não confirmou redução de custo ou tempo. [Abrir o projeto](/projetos/goc-mcp/) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. Produto próprio, desktop ### [md2cv](/projetos/md2cv/) Perfil e versões imutáveis ficam no SQLite da máquina. O agente supervisionado só propõe alterações sob schema; ATS e exportação em PDF/DOCX reutilizam o mesmo grafo, sem backend SaaS dono dos dados. [Abrir o projeto](/projetos/md2cv/) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. [Ver todos os projetos](/projetos/) Percurso ## Uma trajetória de sistemas, não de cargos - 2025 a 2026 ### Arquitetura Plataformas de locação e frotas. A prova pública desta etapa é a mensageria. Infosistemas - 2023 a 2025 ### Analytics em base externa SQL Server de outro time. O dashboard passou a funcionar sem depender de um schema que não controlávamos. VBET - 2023 ### Pós-venda sobre legado O core em PHP 5.7 sustentava a operação. A automação libertou suporte sem reescrever o monólito. Maxmilhas - 2022 a 2023 ### Marketplace multi-tenant White-label para incorporar bancos sem fork por cliente. South System / QUIQ-Itaú - 2021 a 2022 ### Modernização incremental O monólito não tinha os autores originais. A migração começou pelo que o banco ainda deixava ler. Flapper Prática ## Como o trabalho avança Cada etapa restringe a seguinte. Sem essas restrições, arquitetura vira gosto pessoal. Como o trabalho avança - Contexto - Restrição - Decisão - Evidência - Limite 01 ### Contexto Quem sofre quando isso quebra, e em qual operação. 02 ### Restrição O que não pode parar, mudar ou ser tratado como capacidade extra. 03 ### Decisão O caminho escolhido, com a alternativa descartada à vista. 04 ### Evidência O que foi medido, em qual cenário, com que autoria. 05 ### Limite A condição que justificaria rever a decisão. Contato ## Tem um sistema que deixou de ser simples? Chame direto pelo @imrafaeldev, sem formulário — para conversa profissional, comece pelo LinkedIn. [Instagram](https://www.instagram.com/imrafaeldev/) [YouTube](https://www.youtube.com/@imrafaeldev) [GitHub](https://github.com/imrafaeldev) [LinkedIn](https://www.linkedin.com/in/imrafaeldev/) [Abrir a página de contato](/contato/) --- # DiffVision URL: https://imrafaeldev.site/projetos/diffvision > CLI npm local-first para revisar diffs Git, com UI local, comentários no repositório, exportação em Markdown/JSON e servidor MCP. A revisão visual por IA permanece mock. - [Início](/) - [Projetos](/projetos/) - DiffVision CLI pública; revisão por IA ainda mock ## DiffVision O diff Git abre na UI local; comentários e exportação em Markdown ficam no repositório. A revisão visual por IA permanece mock. [Repositório](https://github.com/imrafaeldev/diffvision-app) Revisão local-first Diff no disco, UI local, comentários e export em `.diffvision/`. A plataforma remota fica de fora; a revisão por IA visual ainda é mock. - [01Problema](#problema) - [02Restrições](#restricoes) - [03Decisão](#decisao) - [04Estado atual](#estado-atual) - [05Limitações](#limitacoes) ## Problema Revisar um diff Git em ferramenta SaaS envia código para fora e mistura UI remota com o histórico local. A revisão precisa funcionar offline, com hunks, filtros, bookmarks e comentários ancorados em linhas. ## Restrições Preferências e relatórios devem viver no próprio repositório (.diffvision/). A CLI npm inicia backend e UI locais. Integração com assistente não pode ser vendida como pronta se ainda for protótipo. ## Decisão CLI inspeciona o Git, interpreta diff unificado e sobe interface web. Backend Fastify com snapshot e WebSocket; UI React/Vite. Exportação Markdown/JSON no repositório. Pacote diffvision-mcp por stdio para resumir repositório, ler patches e registrar comentários. O assistente visual de revisão por IA é declarado mock/protótipo; a escrita de comentários via MCP é funcional. ## Estado atual Distribuído como CLI npm, com execução local-first. ## Limitações Não substitui o fluxo de review do GitHub. O fluxo de IA visual não deve ser lido como produto acabado. --- # goc_mcp URL: https://imrafaeldev.site/projetos/goc-mcp > Orquestração local de agentes via MCP em Go: maestro, daemon FIFO, workers OpenCode e SQLite. A entrega funcionou, mas a hipótese de custo e tempo não foi confirmada nesta medição. - [Início](/) - [Projetos](/projetos/) - goc_mcp Experimento documentado ## goc_mcp Maestro (Codex/Cursor) delega via MCP; daemon FIFO e workers OpenCode executam com estado em SQLite. A orquestração funcionou, mas a medição não confirmou redução de custo ou tempo. [Repositório](https://github.com/imrafaeldev/goc_mcp) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. - [01Problema](#problema) - [02Restrições](#restricoes) - [03Decisão](#decisao) - [04Estado atual](#estado-atual) - [05Limitações](#limitacoes) ## Problema Um maestro (Codex ou Cursor) precisa delegar trabalho a workers OpenCode com ciclo de vida explícito: planejar, iniciar, acompanhar, responder, cancelar e recuperar, sem recursão infinita de agentes. ## Restrições Tudo é local. O daemon autentica em loopback. Há limite global e por workspace. Tarefas interrompidas precisam ser reconciliadas. Corrupção de estado não pode virar escrita cega. No caminho feliz: um worker OpenCode por vez, sem decomposição automática que deixe o maestro sem controle. ## Decisão Implementação em Go: gateways MCP por stdio, daemon único, máquina de estados e executor FIFO. Persistência em SQLite com WAL; artefatos em JSONL. Adaptador OpenCode com servidor como caminho principal e CLI como fallback. Isolamento contra delegação recursiva e diagnóstico somente leitura se o estado corromper. ## Estado atual Repositório com testes, ADRs e benchmarks. A orquestração funcionou. As medições publicadas no próprio projeto não confirmaram a hipótese de reduzir tempo e custo. ## Limitações É um experimento. Não afirma ganho de produtividade de engenharia de agentes em produção. O resultado negativo da hipótese faz parte do artefato. --- # Projetos URL: https://imrafaeldev.site/projetos > Artefatos com problema, restrições e estado atual do repositório. - [Início](/) - Projetos ## Projetos Artefatos com problema, restrições e estado atual do repositório. Repositório público ## [SMS Manager](/projetos/sms-manager/) Importar CSV não pode travar a API Nest. A campanha persiste e publica na fila RabbitMQ; consumidores em TypeScript, Go ou Rust gravam o resultado no Mongo. A API não espera a operadora. [Abrir o projeto](/projetos/sms-manager/) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. Experimento documentado ## [goc_mcp](/projetos/goc-mcp/) Maestro (Codex/Cursor) delega via MCP; daemon FIFO e workers OpenCode executam com estado em SQLite. A orquestração funcionou, mas a medição não confirmou redução de custo ou tempo. [Abrir o projeto](/projetos/goc-mcp/) Orquestração local via MCP Maestro delega; daemon FIFO coordena um worker OpenCode por vez; SQLite guarda estado. A hipótese de eficiência não se confirmou. Produto próprio, desktop ## [md2cv](/projetos/md2cv/) Perfil e versões imutáveis ficam no SQLite da máquina. O agente supervisionado só propõe alterações sob schema; ATS e exportação em PDF/DOCX reutilizam o mesmo grafo, sem backend SaaS dono dos dados. [Abrir o projeto](/projetos/md2cv/) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. CLI pública; revisão por IA ainda mock ## [DiffVision](/projetos/diffvision/) O diff Git abre na UI local; comentários e exportação em Markdown ficam no repositório. A revisão visual por IA permanece mock. [Abrir o projeto](/projetos/diffvision/) Revisão local-first Diff no disco, UI local, comentários e export em `.diffvision/`. A plataforma remota fica de fora; a revisão por IA visual ainda é mock. Estação de trabalho editorial ## [Post Engine](/projetos/post-engine/) A entrevista adaptativa extrai evidências; o gateway híbrido (LLM + heurística) barra vivência inventada. Apenas conteúdo confirmado segue para rascunho e exportação em Markdown ou SlideMark. [Abrir o projeto](/projetos/post-engine/) Autoria antes da geração Entrevista extrai evidência; briefing e storyboard preparam o material; o gateway veta fabricado. Só o confirmado segue para rascunho e export. ## Outros trabalhos, outros contextos Projetos visuais para explorar manualmente. - [Gran Goiás Site institucional da Gran Goiás, marmoraria com execução em pedra para obras de escala. Visitar site](https://gran-goias.vercel.app/) - [Gabriel | Nutrição Esportiva Site institucional de Gabriel Pereira para nutrição esportiva, com conteúdo real e estratég](https://gabriel-pereira-nutri.vercel.app/) --- # md2cv URL: https://imrafaeldev.site/projetos/md2cv > Estúdio desktop local-first para perfil profissional, currículos Markdown, versões imutáveis, ATS e adaptação a vagas com agentes supervisionados. Os dados ficam no SQLite da máquina. - [Início](/) - [Projetos](/projetos/) - md2cv Produto próprio, desktop ## md2cv Perfil e versões imutáveis ficam no SQLite da máquina. O agente supervisionado só propõe alterações sob schema; ATS e exportação em PDF/DOCX reutilizam o mesmo grafo, sem backend SaaS dono dos dados. [Repositório](https://github.com/imrafaeldev/md2cv) Grafo local e agente supervisionado Perfil e versões no SQLite; candidatura no mesmo contexto; agente só propõe sob schema; ATS e export reutilizam o grafo sem SaaS dono. - [01Problema](#problema) - [02Restrições](#restricoes) - [03Decisão](#decisao) - [04Estado atual](#estado-atual) - [05Limitações](#limitacoes) ## Problema Perfis profissionais espalham-se entre docs, LinkedIn e exports. Cada candidatura pede um ângulo diferente. Adaptar o currículo com um LLM sem fronteira inventa experiência e apaga o contexto da versão anterior. É preciso um grafo versionado na máquina que pergunte o que falta e rejeite saída incompleta, sem prometer contratação ou aprovação automática por ATS. ## Restrições Desktop e local-first (Electron): sem conta proprietária e sem backend dono dos dados. - IPC tipado e validado entre renderer e processo principal. - SQLite com foreign keys, WAL, migrations com checksum, verificações de integridade e backup antes de operação destrutiva. - Agentes (Codex, Cursor, OpenCode) entram por adaptadores isolados, não como donos do banco. - Adaptação a candidatura não grava no perfil sem confirmação explícita. - Acesso externo só sob ação do usuário (pesquisa de URL de empresa ou CLI de IA já configurada no computador); o produto não armazena credenciais de provedores. ## Decisão Renderer React/Vite separado do processo principal. O produto organiza o trabalho em um único grafo local: - Perfil: experiências, formação, cursos, idiomas, projetos, links, habilidades e empresas por pessoa. - Currículos: Markdown, versões imutáveis, restauração rastreável e panorama de evolução. - ATS: diagnósticos estruturais e pontuação orientativa; PDF textual marcado e DOCX semântico. - Candidaturas: empresa, vaga, currículo base, versão e estado no mesmo contexto. - Agentes: máquina de estados para perguntas, tentativas e proposta de nova versão sob schema; só persiste com confirmação. - Dados: importação e exportação versionada do grafo completo; compilador Markdown (unified/remark) compartilhado por preview, auditoria e export. Fluxo canônico: perfil → currículo base → versão imutável → auditoria ATS → PDF/DOCX. O ramo de candidatura passa por agente local supervisionado antes do currículo adaptado. ## Estado atual Repositório público sob MIT, portal de documentação e releases x64 para Linux (AppImage, .deb, .rpm) e Windows (NSIS e portátil), com CI e testes Vitest e Playwright em persistência, agentes, ATS e fluxos Electron. O export profissional usado neste site nasce desse produto e não é lido pelo site em runtime. ## Limitações Não substitui revisão humana nem promete contratação ou aprovação automática por plataformas de recrutamento. A adaptação a vagas depende de respostas confirmadas; o sistema recusa fabricar experiência. Binários Windows desta fase não possuem assinatura de código. O projeto é autoral e sem finalidade comercial do mantenedor; a licença MIT permite reutilização nos seus termos. --- # Post Engine URL: https://imrafaeldev.site/projetos/post-engine > Workstation editorial centrada em autoria: entrevista, briefing, storyboard e exportação após o gateway barrar conteúdo fabricado. Prompts versionados; workspace LLM isolado. - [Início](/) - [Projetos](/projetos/) - Post Engine Estação de trabalho editorial ## Post Engine A entrevista adaptativa extrai evidências; o gateway híbrido (LLM + heurística) barra vivência inventada. Apenas conteúdo confirmado segue para rascunho e exportação em Markdown ou SlideMark. [Repositório](https://github.com/imrafaeldev/post-engine) Autoria antes da geração Entrevista extrai evidência; briefing e storyboard preparam o material; o gateway veta fabricado. Só o confirmado segue para rascunho e export. - [01Problema](#problema) - [02Restrições](#restricoes) - [03Decisão](#decisao) - [04Estado atual](#estado-atual) - [05Limitações](#limitacoes) ## Problema Gerar post “profissional” com LLM a partir de um slogan inventa biografia. O fluxo precisa entrevistar, identificar lacunas, preparar o briefing, redigir e exportar, e recusar o que não foi confirmado. ## Restrições Núcleo em Python com fronteiras entre entrevista, geração, preservação de autoria, segmentação e persistência. Chamadas a modelo em workspace isolado, allowlist de provedor e prompts como contratos versionados. Avaliação híbrida: LLM mais heurísticas determinísticas. Ausência de experiência nunca vira falsa vivência. ## Decisão Entrevistas adaptativas, briefing autoral, storyboard, veto a conteúdo fabricado, registry SQLite de prompts com rollback, interface textual e frontend React/Vite para revisar fases. Exportação Markdown ou SlideMark JSON após avaliação. ## Estado atual Base com testes de entrevista, isolamento de LLM, registry, persistência e conversão SlideMark. ## Limitações Não é um gerador genérico de thought leadership. Sem repertório real, o sistema recusa; não completa a biografia. --- # SMS Manager URL: https://imrafaeldev.site/projetos/sms-manager > Campanhas de SMS desacopladas: a API Nest persiste e publica, a fila entrega e consumidores em TypeScript, Go ou Rust gravam o resultado. Token opaco, Redis e gRPC entre serviços. - [Início](/) - [Projetos](/projetos/) - SMS Manager Repositório público ## SMS Manager Importar CSV não pode travar a API Nest. A campanha persiste e publica na fila RabbitMQ; consumidores em TypeScript, Go ou Rust gravam o resultado no Mongo. A API não espera a operadora. [Repositório](https://github.com/imrafaeldev/sms-manager) Desacoplamento da campanha CSV na API Nest com persistência Postgres; publicação na fila; consumidores no mesmo contrato gravam no Mongo. A API não espera a operadora. - [01Problema](#problema) - [02Restrições](#restricoes) - [03Decisão](#decisao) - [04Estado atual](#estado-atual) - [05Limitações](#limitacoes) ## Problema Campanhas de SMS partem de arquivos CSV, usuários, empresas e autenticação entre serviços. Validar e persistir no mesmo processo que dispara milhares de mensagens acopla a API ao ritmo da operadora e da fila. ## Restrições O ambiente precisa ser reproduzível. Autenticação entre serviços não pode depender de JWT opaco sem revogação. Consumidores em mais de uma linguagem existem para comparar o mesmo contrato de mensageria. ## Decisão Sete aplicações: APIs NestJS de usuários/autenticação e de empresas/campanhas; consumidores equivalentes em Node.js/TypeScript, Go e Rust; provisionador declarativo de exchanges, filas e bindings; gerador de massa CSV. PostgreSQL/TypeORM para dados relacionais na API; MongoDB para resultado do consumo; Redis para cache de token opaco (revogável); gRPC para autenticação entre serviços; RabbitMQ com exchanges topic para os lotes. A API publica e segue sem bloquear no ritmo da operadora. ## Estado atual Repositório público com Docker Compose para PostgreSQL, MongoDB, Redis e RabbitMQ. A arquitetura separa domínio, aplicação e infraestrutura nos contextos de usuários, autenticação e empresas; os três consumidores implementam o mesmo contrato de mensagem. ## Limitações É um artefato de estudo e operação local de mensageria, não um produto comercial com SLA de operadora. Os três consumidores demonstram o contrato; não afirmam que as três linguagens rodam juntas em produção do autor. --- # Most backend performance problems start close to the data Source: articles/backend-performance-perto-dos-dados-en.md > Before adding machines, cache, or queues, measure the request's work. Execution plans, N+1, CAST on columns, and sequential loops. A slow API and the meeting already fills with solutions before anyone has a measurement. More CPU and RAM show up, cache, queues, microservices, refactoring. Sometimes someone even proposes switching languages. Rarely is the first suggestion to open the execution plan. That is why I start the investigation close to the data. Not because the database is the default culprit, but because so much passes through it and that check usually gives a fast signal. If the query is healthy, I rule the database out and follow the request flow. ### Quick fixes can also hide expensive work Cache and queues solve real problems. Bigger machines can also be the right call. When they arrive by reflex, though, those resources may only change the bill size while nobody knows which part of the request is holding the response. More CPU reduces resource contention, cache takes some requests out of the path, and a queue absorbs a spike. Meanwhile, a query doing enormous work to return almost nothing stays expensive on every execution. The same goes for a loop firing one call per item. Latency may drop for a while, the alert stops firing, and the team breathes. When load grows, the expensive operation reappears, now accompanied by larger infrastructure. The question that must come before the architecture debate: how much work is this request producing to deliver the result? ### We almost doubled the machine and the dashboard stayed slow That is what happened with a dashboard I worked on. It loaded synchronously, and a single query did everything at once: fetched data, joined several pieces of information, and computed the displayed values. Since database CPU ran very high, we almost doubled CPU and RAM. The database got bigger. The dashboard still took over a minute to load. The same query still concentrated all heavy work in a single execution. That was when we opened the execution plan. The screen returned few indicators, but the query crossed relationships, formed a large intermediate volume, and spent CPU on aggregations before reaching them. The final answer was small. The work to produce it, enormous. Doubling CPU and RAM had given the database more headroom, but the search still forced it to do everything at once. The plan showed the investigation had to enter the path traveled by the query. ### What I need to see before touching the database For the database to become a real suspect, I want the execution plan and metrics pointing that way. Query time, reads, and cardinality usually confirm or eliminate a hypothesis in minutes. If those measures are healthy, I rule the database out. I have caught slow APIs with the query responding within expectations, while the delay sat in application processing and sequential remote calls. From there, continuing to hunt a database defect would only insist on the wrong layer. Before going deeper, I run a short radar over the query and the code. One query to load the list followed by another per record calls for a query count. That N+1 shows up often when the ORM leaves relationships for the backend to fetch one by one. A JOIN multiplying rows before aggregation calls for measuring intermediate volume. A missing index calls for the plan. A filter applying a function over the indexed column too. In code, I look for remote calls or queries with `await` inside a `for`. Each wait may look small alone and still dominate total time when all run in a queue. None of these signals closes the diagnosis. An N+1 on a route with two items may have irrelevant impact, a new index may help little on a low-selectivity column, and a voluminous join may be needed to produce the result. So I count queries, time the whole loop, and check in the plan how many rows and reads were produced. That radar only picks the first measurement. The next investigation layer comes from the math. ### One correct line can waste the index One of those lines often passes review unnoticed. It returns a full day's records. ```sql WHERE CAST(datetime_column AS date) = @date ``` The screen result looks correct. In the plan, the story may differ. Applying `CAST` to the column forces the query to transform values before comparison. With an index on `datetime_column`, that can prevent a direct range seek, raise reads substantially, and even lead to a scan. When the request is for a full day's records, I compute the boundaries outside the column. ```sql WHERE datetime_column >= @start_of_day AND datetime_column < @start_of_next_day ``` If the start is July 16 at midnight, the next bound is July 17 at midnight. That includes every value on the 16th, including those with fractional seconds at the end, without depending on `23:59:59.999`. The result stays correct, but now there is a range the index can walk. Confirmation comes from comparing reads and the access operator in both plans. If the metric does not change, the hypothesis did not hold. ### Read the plan by the work sequence After comparing filter versions, I read the plan as a story of the work produced by the query. I start with total time and reads. If the screen returns ten indicators but execution performs hundreds of thousands of reads, there is a bill to explain. Then I compare estimated vs actual cardinality. When the optimizer expected few rows and received many, it may have picked joins, memory, and aggregations for a far smaller scenario than it met at runtime. Next I follow where data grows. I look for the operator where a JOIN takes thousands of rows to hundreds of thousands, just before a filter or `GROUP BY` shrinks everything again. The small final answer may hide an enormous intermediate volume. That is where aggregations and sorts start spending too much CPU. I also do not take the plan's displayed cost percentage as verdict. It serves to choose where to measure. I mark the expensive operator, how many rows enter and leave it, and how much time it consumes. If cost shows up after row multiplication and before the few needed indicators, there is already a concrete hypothesis to test. ### Split the query where cost grows What fixed the dashboard was splitting the query and using indexes properly. The split did not happen arbitrarily. The plan itself showed where cost started growing. We kept in the database the filters and indexed lookup of the needed records. Moving that slice to the application would make the server receive a larger set before it could discard it. It would also waste the path indexes already shortened. Final indicator composition moved to the application. That stage came after the relationships and aggregations that raised intermediate volume and kept database CPU high. With data already sliced, the server could assemble screen values without concentrating all execution in a single query. The database started reducing the set early. The application received the selected records and composed the indicators. After the change, the dashboard stopped depending on that heavy query and became more predictable, because the split followed the point where cost grew in the plan. That boundary did not come from a preference for single queries or for more application logic. I marked where CPU, reads, and intermediate rows spiked after selective filters. Then I checked whether the expensive stretch could receive an already-reduced set outside the database. Before moving that stage, two bills had to balance. The database should keep doing the slicing that used the indexes. The application should compose the result without fetching too many rows, creating per-item calls, or turning the network into the new bottleneck. Validation had to show less database CPU and reads without raising route payload or latency. Without that improvement, the split would only transfer cost to another layer and the dashboard would stay expensive. ### When the plan points outside the database On another route, the plan showed a healthy query, with reads and time within expectations. Still, the API stayed slow. When I measured the whole flow, the delay appeared after the query, in application processing and sequential remote calls. A stretch like this changes the math. ```typescript for (const item of items) { await fetchDetail(item.id); } ``` With 50 items and 40 ms per call, the loop can add about two seconds to the route. Each `await` starts the next call only after the previous response. The 40 ms looks small alone. Total time shows fifty waits in a queue. When profiling points at that path, I change the code and the flow design. I separate operations depending on the previous response from those that can run together. For the independent ones, I test bounded concurrency. Firing fifty requests at once can also just move pressure to the integration. The change must be validated by total route time, error rate, and remote-service saturation. Improving one measure while another explodes does not fix the problem. ### Two hours to locate the layer, one day to test "We found the bottleneck" too often becomes "we fixed the slowness" too early. After locating a bad query or sequential stretch, the change, deploy, and production confirmation are still missing. Finding the layer only shortens the suspect list. In the first two hours, I want to locate where to dig deeper: database, application, integration, or infrastructure. I open the plan, compare time and reads, count queries, and time the stretch running after the database. If the plan is healthy and fifty 40 ms calls add up to about two seconds, I stop hunting indexes and measure the code. If reads explode before aggregation, the code waits while I test the query. That window is an investigation ruler, not a guarantee. On the slow dashboard, I spent about three hours in the terminal looking at the plan, comparing metrics, and testing the query split. The rest of the day involved meetings, access, deploy, and production confirmation. So on day one I want at least one tested hypothesis with before-and-after measurement. The test may rewrite the filter without `CAST`, split the query, or bound the loop's concurrency. After deploy, I compare the metric that motivated the change: reads and CPU for the database; latency, errors, and saturation for the route. With intermittent failures, that window grows. You must wait for the symptom to reappear with enough metrics to compare. ### Confirmation happens after deploy After some production mistakes, I stopped looking for a default culprit. Database, application, and infrastructure enter as hypotheses, each with a measure that can confirm or discard it. When the plan shows high reads and cardinality far from reality, I work on the query. When the query answers well and profiling concentrates time after it, I move to code or integration. Cache, queues, indexes, and bigger machines stay available. They are not forbidden decisions. They just should not arrive before we know which work is holding the response. Before changing, I define the measure that must improve and the one that must not worsen. After deploy, I return to the plan, metrics, and profiling. If the target metric does not improve, the hypothesis did not hold. If the guardrail measure worsens, the fix created another problem. I discard the hypothesis and return to the flow point where time still grows. --- *Originally published on [LinkedIn](https://www.linkedin.com/pulse/maioria-dos-problemas-de-performance-backend-come%C3%A7a-perto-s-pereira-zqlae/) on July 16, 2026.* --- # La mayoría de los problemas de performance de backend empieza cerca de los datos Source: articles/backend-performance-perto-dos-dados-es.md > Antes de subir máquina, caché o cola, mide el trabajo de la petición. Plan de ejecución, N+1, CAST en columna y loops secuenciales. Una API se vuelve lenta y la reunión ya se llena de soluciones antes de ganar una medición. Aparecen más CPU y RAM, caché, cola, microservicios y refactorización. A veces alguien propone hasta cambiar el lenguaje. Raramente la primera sugerencia es abrir el plan de ejecución. Por eso, empiezo la investigación cerca de los datos. No porque la base sea la culpable por defecto, sino porque mucho pasa por ahí y esa verificación suele dar señal rápida. Si la consulta está saludable, saco la base del frente y sigo el flujo de la petición. ### Soluciones rápidas también esconden trabajo caro Caché y cola resuelven problemas reales. Más máquina también puede ser la decisión correcta. Cuando entran por reflejo, sin embargo, esos recursos pueden solo cambiar el tamaño de la cuenta mientras nadie sabe qué tramo de la petición está reteniendo la respuesta. Subir CPU reduce la disputa por recurso, la caché saca algunas peticiones del camino y la cola absorbe un pico. Mientras tanto, una consulta que hace un trabajo enorme para devolver casi nada sigue cara en cada ejecución. Lo mismo vale para un loop que dispara una llamada por ítem. La latencia puede caer por un tiempo, la alerta deja de sonar y el equipo respira. Cuando la carga crece, la operación cara reaparece, ahora acompañada de una infraestructura mayor. La pregunta que debe venir antes de la discusión de arquitectura: ¿cuánto trabajo está produciendo esta petición para entregar el resultado? ### Casi duplicamos la máquina y el dashboard siguió lento Fue lo que pasó en un dashboard en el que trabajé. Cargaba de forma síncrona, y una única consulta hacía todo a la vez: buscaba los datos, cruzaba varias informaciones y calculaba los valores exhibidos. Como la CPU de la base quedaba muy alta, casi duplicamos CPU y RAM. La base quedó más grande. El dashboard siguió pasando del minuto para cargar. La misma consulta aún concentraba todo el trabajo pesado en una única ejecución. Fue cuando abrimos el plan de ejecución. La pantalla devolvía pocos indicadores, pero la consulta atravesaba relaciones, formaba un volumen intermedio grande y gastaba CPU en agregaciones antes de llegar a ellos. La respuesta final era pequeña. El trabajo para producirla, enorme. Duplicar CPU y RAM había dado más aire a la base, pero la búsqueda seguía obligándola a hacer todo a la vez. El plan mostró que la investigación necesitaba entrar en el camino recorrido por la consulta. ### Lo que necesito ver antes de tocar la base Para que la base se vuelva sospechosa de verdad, quiero el plan de ejecución y las métricas apuntando en esa dirección. Tiempo de la consulta, lecturas y cardinalidad suelen confirmar o eliminar una hipótesis en pocos minutos. Si esas medidas están saludables, saco la base del frente. Ya tomé API lenta con la consulta respondiendo dentro de lo esperado, mientras el retraso estaba en el procesamiento de la aplicación y en llamadas remotas hechas de forma secuencial. A partir de ahí, seguir buscando un defecto en la base sería solo insistir en la capa equivocada. Antes de profundizar, paso un radar corto por la consulta y por el código. Una query para cargar la lista seguida de otra para cada registro pide un conteo de consultas. Ese N+1 aparece con frecuencia cuando el ORM deja las relaciones para que el backend las busque una a una. Un JOIN que multiplica filas antes de la agregación pide la medición del volumen intermedio. Índice ausente pide plan. Un filtro que aplica una función sobre la columna indexada también. En el código, busco llamadas remotas o consultas con `await` dentro de `for`. Cada espera puede parecer pequeña aisladamente y aun así dominar el tiempo total cuando todas ejecutan en fila. Ninguna de estas señales cierra el diagnóstico. Un N+1 en una ruta con dos ítems puede tener impacto irrelevante, un índice nuevo puede ayudar poco en una columna con baja selectividad y un join voluminoso quizás sea necesario para producir el resultado. Por eso, cuento las consultas, cronometro el loop entero y reviso en el plan cuántas filas y lecturas se produjeron. Ese radar solo elige la primera medición. La próxima capa de la investigación viene de la cuenta. ### Una línea correcta puede desperdiciar el índice Una de esas líneas suele pasar desapercibida en la revisión. Devuelve los registros de un día entero. ```sql WHERE CAST(campo_data_hora AS date) = @data ``` El resultado de la pantalla parece correcto. En el plan, la historia puede ser otra. Aplicar `CAST` a la columna obliga a la consulta a transformar los valores antes de la comparación. Con un índice en `campo_data_hora`, esto puede impedir una búsqueda directa por el intervalo, aumentar bastante las lecturas e incluso llevar a un scan. Cuando el pedido es por los registros de un día entero, calculo los bordes fuera de la columna. ```sql WHERE campo_data_hora >= @inicio_do_dia AND campo_data_hora < @inicio_do_proximo_dia ``` Si el inicio es el 16 de julio a la medianoche, el límite siguiente es el 17 de julio a la medianoche. Así entran todos los valores del día 16, incluso aquellos con fracciones de segundo al final, sin depender de `23:59:59.999`. El resultado permanece correcto, pero ahora existe un intervalo que el índice puede recorrer. La confirmación viene de la comparación de las lecturas y del operador de acceso en los dos planes. Si la métrica no cambia, la hipótesis no se sostuvo. ### Lee el plan por la secuencia del trabajo Después de comparar las versiones del filtro, leo el plan como una historia del trabajo producido por la consulta. Empiezo por el tiempo total y por las lecturas. Si la pantalla devuelve diez indicadores, pero la ejecución hace cientos de miles de lecturas, hay una cuenta por explicar. Después comparo la cardinalidad estimada con la real. Cuando el optimizador esperaba pocas filas y recibió muchas, puede haber elegido joins, memoria y agregaciones para un escenario bien menor que el encontrado durante la ejecución. Enseguida acompaño dónde crecen los datos. Busco el operador en que un JOIN lleva miles de filas a cientos de miles, poco antes de que un filtro o `GROUP BY` lo reduzca todo de nuevo. La respuesta final pequeña puede esconder un volumen intermedio enorme. Es en ese camino donde agregaciones y ordenaciones empiezan a gastar demasiada CPU. Tampoco tomo el porcentaje de costo exhibido por el plan como sentencia. Sirve para elegir dónde medir. Marco el operador caro, cuántas filas entran y salen de él y cuánto tiempo consume. Si el costo aparece después de la multiplicación de las filas y antes de los pocos indicadores necesarios, ya existe una hipótesis concreta para probar. ### Romper la consulta donde crece el costo Lo que resolvió el dashboard fue romper la consulta y aprovechar los índices correctamente. La división no ocurrió de forma arbitraria. El propio plan mostró dónde el costo empezaba a crecer. Mantuvimos en la base los filtros y la búsqueda indexada de los registros necesarios. Llevar ese recorte a la aplicación haría que el servidor recibiera un conjunto mayor antes de poder descartarlo. También desperdiciaría el camino que los índices ya acortaban. La composición final de los indicadores fue a la aplicación. Esa etapa venía después de las relaciones y agregaciones que elevaban el volumen intermedio y mantenían alta la CPU de la base. Con los datos ya recortados, el servidor podía montar los valores de la pantalla sin concentrar toda la ejecución en una única query. La base pasó a reducir el conjunto temprano. La aplicación recibía los registros seleccionados y componía los indicadores. Tras el cambio, el dashboard dejó de depender de aquella consulta pesada y quedó más predecible, porque la división acompañaba el punto en que el costo crecía en el plan. Esa frontera no vino de una preferencia por query única o por más lógica en la aplicación. Marqué dónde CPU, lecturas y filas intermedias se disparaban después de los filtros selectivos. Entonces verifiqué si el tramo caro podría recibir fuera de la base un conjunto ya reducido. Antes de mover esa etapa, dos cuentas necesitaban cerrar. La base debía seguir haciendo el recorte que aprovechaba los índices. La aplicación debía componer el resultado sin buscar demasiadas filas, crear llamadas por ítem o transformar la red en el nuevo cuello de botella. La validación necesitaba mostrar menos CPU y lecturas en la base sin aumentar el payload ni la latencia de la ruta. Sin esa mejora, la división solo transferiría el costo a otra capa y el dashboard seguiría caro. ### Cuando el plan apunta fuera de la base En otra ruta, el plan mostraba una consulta saludable, con lecturas y tiempo dentro de lo esperado. Aun así, la API seguía lenta. Cuando medí el flujo entero, el retraso apareció después de la consulta, en el procesamiento de la aplicación y en llamadas remotas ejecutadas en secuencia. Un tramo así cambia la cuenta. ```typescript for (const item of itens) { await buscarDetalhe(item.id); } ``` Con 50 ítems y 40 ms por llamada, el loop puede añadir cerca de dos segundos a la ruta. Cada `await` inicia la próxima llamada solo después de la respuesta anterior. Los 40 ms parecen pequeños vistos solos. El tiempo total muestra las cincuenta esperas en fila. Cuando el profiling apunta ese camino, toco el código y el diseño del flujo. Separo las operaciones que dependen de la respuesta anterior de aquellas que pueden correr juntas. Para las independientes, pruebo concurrencia limitada. Disparar cincuenta peticiones a la vez también puede solo desplazar la presión hacia la integración. El cambio debe validarse por el tiempo total de la ruta, por la tasa de errores y por la saturación del servicio remoto. Mejorar una medida mientras otra explota no resuelve el problema. ### Dos horas para localizar la capa, un día para probar "Encontramos el cuello de botella" suele volverse "resolvimos la lentitud" demasiado pronto. Después de localizar una consulta mala o un tramo secuencial, aún faltan el cambio, el deploy y la confirmación en producción. Encontrar la capa solo acorta la lista de sospechosos. En las primeras dos horas, quiero localizar dónde profundizar: base, aplicación, integración o infraestructura. Abro el plan, comparo tiempo y lecturas, cuento consultas y cronometro el tramo ejecutado después de la base. Si el plan está saludable y cincuenta llamadas de 40 ms suman cerca de dos segundos, dejo de buscar índice y mido el código. Si las lecturas explotan antes de la agregación, el código espera mientras pruebo la consulta. Ese plazo es una regla de investigación, no una garantía. En el dashboard lento, pasé cerca de tres horas en la terminal mirando el plan, comparando métricas y probando la ruptura de la consulta. El resto del día involucró reunión, acceso, deploy y confirmación en producción. Por eso, en el primer día quiero al menos una hipótesis probada con medida de antes y después. La prueba puede reescribir el filtro sin `CAST`, dividir la query o limitar la concurrencia del loop. Después del deploy, comparo la métrica que motivó el cambio: lecturas y CPU para la base; latencia, errores y saturación para la ruta. En fallos intermitentes, esa ventana crece. Hay que esperar que el síntoma reaparezca con métricas suficientes para comparar. ### La confirmación ocurre después del deploy Después de algunos errores en producción, dejé de buscar un culpable estándar. Base, aplicación e infraestructura entran como hipótesis, cada una acompañada de una medida que puede confirmarla o descartarla. Cuando el plan muestra lecturas altas y cardinalidad distante de la realidad, trabajo en la consulta. Cuando la consulta responde bien y el profiling concentra tiempo después de ella, sigo hacia el código o hacia la integración. Caché, cola, índice y máquina mayor siguen disponibles. No son decisiones prohibidas. Solo no deberían entrar antes de saber qué trabajo está reteniendo la respuesta. Antes de cambiar, defino la medida que debe mejorar y la que no puede empeorar. Después del deploy, vuelvo al plan, a las métricas y al profiling. Si la métrica objetivo no mejora, la hipótesis no se sostuvo. Si la medida de protección empeora, la solución creó otro problema. Descarto la hipótesis y regreso al punto del flujo en que el tiempo aún crece. --- *Publicado originalmente en [LinkedIn](https://www.linkedin.com/pulse/maioria-dos-problemas-de-performance-backend-come%C3%A7a-perto-s-pereira-zqlae/) el 16 de julio de 2026.* --- # A maioria dos problemas de performance de backend começa perto dos dados Source: articles/backend-performance-perto-dos-dados-pt-br.md > Antes de subir máquina, cache ou fila, meça o trabalho da requisição. Plano de execução, N+1, CAST em coluna e loops sequenciais. Uma API fica lenta e a reunião já enche de soluções antes de ganhar uma medição. Aparecem mais CPU e RAM, cache, fila, microserviços e refatoração. Às vezes alguém propõe até trocar a linguagem. Raramente a primeira sugestão é abrir o plano de execução. Por isso, começo a investigação perto dos dados. Não porque o banco seja o culpado por padrão, mas porque muita coisa passa por ali e essa verificação costuma dar sinal rápido. Se a consulta estiver saudável, tiro o banco da frente e sigo o fluxo da requisição. ### Soluções rápidas também escondem trabalho caro Cache e fila resolvem problemas reais. Mais máquina também pode ser a decisão certa. Quando entram por reflexo, porém, esses recursos podem apenas mudar o tamanho da conta enquanto ninguém sabe qual trecho da requisição está segurando a resposta. Subir CPU reduz a disputa por recurso, cache tira algumas requisições do caminho e fila absorve um pico. Enquanto isso, uma consulta que faz um trabalho enorme para retornar quase nada continua cara em cada execução. O mesmo vale para um loop que dispara uma chamada por item. A latência pode cair por um tempo, o alerta para de tocar e o time respira. Quando a carga cresce, a operação cara reaparece, agora acompanhada de uma infraestrutura maior. A pergunta que precisa vir antes da discussão de arquitetura: quanto trabalho essa requisição está produzindo para entregar o resultado? ### Quase dobramos a máquina e o dashboard continuou lento Foi o que aconteceu em um dashboard em que trabalhei. Ele carregava de forma síncrona, e uma única consulta fazia tudo de uma vez: buscava os dados, cruzava várias informações e calculava os valores exibidos. Como a CPU do banco ficava muito alta, quase dobramos CPU e RAM. O banco ficou maior. O dashboard continuou passando de um minuto para carregar. A mesma consulta ainda concentrava todo o trabalho pesado em uma única execução. Foi quando abrimos o plano de execução. A tela devolvia poucos indicadores, mas a consulta atravessava relacionamentos, formava um volume intermediário grande e gastava CPU em agregações antes de chegar neles. A resposta final era pequena. O trabalho para produzi-la, enorme. Dobrar CPU e RAM tinha dado mais fôlego ao banco, mas a busca continuava obrigando-o a fazer tudo de uma vez. O plano mostrou que a investigação precisava entrar no caminho percorrido pela consulta. ### O que preciso ver antes de mexer no banco Para o banco virar suspeito de verdade, quero o plano de execução e as métricas apontando nessa direção. Tempo da consulta, leituras e cardinalidade geralmente confirmam ou eliminam uma hipótese em poucos minutos. Se essas medidas estiverem saudáveis, tiro o banco da frente. Já peguei API lenta com a consulta respondendo dentro do esperado, enquanto o atraso estava no processamento da aplicação e em chamadas remotas feitas de forma sequencial. A partir daí, continuar procurando um defeito no banco seria apenas insistir na camada errada. Antes de aprofundar, passo um radar curto pela consulta e pelo código. Uma query para carregar a lista seguida de outra para cada registro pede uma contagem de consultas. Esse N+1 aparece com frequência quando o ORM deixa as relações para o backend buscar uma a uma. Um JOIN que multiplica linhas antes da agregação pede a medição do volume intermediário. Índice ausente pede plano. Um filtro que aplica uma função sobre a coluna indexada também. No código, procuro chamadas remotas ou consultas com `await` dentro de `for`. Cada espera pode parecer pequena isoladamente e ainda assim dominar o tempo total quando todas executam em fila. Nenhum desses sinais fecha o diagnóstico. Um N+1 numa rota com dois itens pode ter impacto irrelevante, um índice novo pode ajudar pouco numa coluna com baixa seletividade e um join volumoso talvez seja necessário para produzir o resultado. Por isso, conto as consultas, cronometro o loop inteiro e confiro no plano quantas linhas e leituras foram produzidas. Esse radar apenas escolhe a primeira medição. A próxima camada da investigação vem da conta. ### Uma linha correta pode desperdiçar o índice Uma dessas linhas costuma passar batida na revisão. Ela devolve os registros de um dia inteiro. ```sql WHERE CAST(campo_data_hora AS date) = @data ``` O resultado da tela parece correto. No plano, a história pode ser outra. Aplicar `CAST` à coluna obriga a consulta a transformar os valores antes da comparação. Com um índice em `campo_data_hora`, isso pode impedir uma busca direta pelo intervalo, aumentar bastante as leituras e até levar a um scan. Quando o pedido é pelos registros de um dia inteiro, calculo as bordas fora da coluna. ```sql WHERE campo_data_hora >= @inicio_do_dia AND campo_data_hora < @inicio_do_proximo_dia ``` Se o início é 16 de julho à meia-noite, o limite seguinte é 17 de julho à meia-noite. Assim entram todos os valores do dia 16, inclusive aqueles com frações de segundo no final, sem depender de `23:59:59.999`. O resultado permanece correto, mas agora existe um intervalo que o índice pode percorrer. A confirmação vem da comparação das leituras e do operador de acesso nos dois planos. Se a métrica não mudar, a hipótese não ficou de pé. ### Leia o plano pela sequência do trabalho Depois de comparar as versões do filtro, leio o plano como uma história do trabalho produzido pela consulta. Começo pelo tempo total e pelas leituras. Se a tela devolve dez indicadores, mas a execução faz centenas de milhares de leituras, existe uma conta para explicar. Depois comparo a cardinalidade estimada com a real. Quando o otimizador esperava poucas linhas e recebeu muitas, ele pode ter escolhido joins, memória e agregações para um cenário bem menor do que encontrou durante a execução. Em seguida acompanho onde os dados crescem. Procuro o operador em que um JOIN leva milhares de linhas a centenas de milhares, pouco antes de um filtro ou `GROUP BY` reduzir tudo novamente. A resposta final pequena pode esconder um volume intermediário enorme. É nesse caminho que agregações e ordenações começam a gastar CPU demais. Também não tomo o percentual de custo exibido pelo plano como sentença. Ele serve para escolher onde medir. Marco o operador caro, quantas linhas entram e saem dele e quanto tempo ele consome. Se o custo aparece depois da multiplicação das linhas e antes dos poucos indicadores necessários, já existe uma hipótese concreta para testar. ### Quebrar a consulta onde o custo cresce O que resolveu o dashboard foi quebrar a consulta e aproveitar os índices corretamente. A divisão não aconteceu de forma arbitrária. O próprio plano mostrou onde o custo começava a crescer. Mantivemos no banco os filtros e a busca indexada dos registros necessários. Levar esse recorte para a aplicação faria o servidor receber um conjunto maior antes de poder descartá-lo. Também desperdiçaria o caminho que os índices já encurtavam. A composição final dos indicadores foi para a aplicação. Essa etapa vinha depois dos relacionamentos e das agregações que elevavam o volume intermediário e mantinham alta a CPU do banco. Com os dados já recortados, o servidor podia montar os valores da tela sem concentrar toda a execução em uma única query. O banco passou a reduzir o conjunto cedo. A aplicação recebia os registros selecionados e compunha os indicadores. Depois da mudança, o dashboard deixou de depender daquela consulta pesada e ficou mais previsível, porque a divisão acompanhava o ponto em que o custo crescia no plano. Essa fronteira não veio de uma preferência por query única ou por mais lógica na aplicação. Marquei onde CPU, leituras e linhas intermediárias disparavam depois dos filtros seletivos. Então verifiquei se o trecho caro poderia receber fora do banco um conjunto já reduzido. Antes de mover essa etapa, duas contas precisavam fechar. O banco deveria continuar fazendo o recorte que aproveitava os índices. A aplicação deveria compor o resultado sem buscar linhas demais, criar chamadas por item ou transformar a rede no novo gargalo. A validação precisava mostrar menos CPU e leituras no banco sem aumentar o payload ou a latência da rota. Sem essa melhora, a divisão apenas transferiria o custo para outra camada e o dashboard continuaria caro. ### Quando o plano aponta para fora do banco Em outra rota, o plano mostrava uma consulta saudável, com leituras e tempo dentro do esperado. Mesmo assim, a API continuava lenta. Quando medi o fluxo inteiro, o atraso apareceu depois da consulta, no processamento da aplicação e em chamadas remotas executadas em sequência. Um trecho assim muda a conta. ```typescript for (const item of itens) { await buscarDetalhe(item.id); } ``` Com 50 itens e 40 ms por chamada, o loop pode acrescentar cerca de dois segundos à rota. Cada `await` inicia a próxima chamada somente depois da resposta anterior. Os 40 ms parecem pequenos quando vistos sozinhos. O tempo total mostra as cinquenta esperas em fila. Quando o profiling aponta esse caminho, mexo no código e no desenho do fluxo. Separo as operações que dependem da resposta anterior daquelas que podem rodar juntas. Para as independentes, testo concorrência limitada. Disparar cinquenta requisições de uma vez também pode apenas deslocar a pressão para a integração. A mudança precisa ser validada pelo tempo total da rota, pela taxa de erros e pela saturação do serviço remoto. Melhorar uma medida enquanto outra explode não resolve o problema. ### Duas horas para localizar a camada, um dia para testar “Achamos o gargalo” costuma virar “resolvemos a lentidão” cedo demais. Depois de localizar uma consulta ruim ou um trecho sequencial, ainda faltam a mudança, o deploy e a confirmação em produção. Encontrar a camada apenas encurta a lista de suspeitos. Nas primeiras duas horas, quero localizar onde aprofundar: banco, aplicação, integração ou infraestrutura. Abro o plano, comparo tempo e leituras, conto consultas e cronometro o trecho executado depois do banco. Se o plano está saudável e cinquenta chamadas de 40 ms somam cerca de dois segundos, paro de procurar índice e meço o código. Se as leituras explodem antes da agregação, o código espera enquanto testo a consulta. Esse prazo é uma régua de investigação, não uma garantia. No dashboard lento, passei cerca de três horas no terminal olhando o plano, comparando métricas e testando a quebra da consulta. O restante do dia envolveu reunião, acesso, deploy e confirmação em produção. Por isso, no primeiro dia quero ao menos uma hipótese testada com medida de antes e depois. O teste pode reescrever o filtro sem `CAST`, dividir a query ou limitar a concorrência do loop. Depois do deploy, comparo a métrica que motivou a mudança: leituras e CPU para o banco; latência, erros e saturação para a rota. Em falhas intermitentes, essa janela cresce. É preciso esperar o sintoma reaparecer com métricas suficientes para comparar. ### A confirmação acontece depois do deploy Depois de alguns erros em produção, parei de procurar um culpado padrão. Banco, aplicação e infraestrutura entram como hipóteses, cada uma acompanhada de uma medida que pode confirmá-la ou descartá-la. Quando o plano mostra leituras altas e cardinalidade distante da realidade, trabalho na consulta. Quando a consulta responde bem e o profiling concentra tempo depois dela, sigo para o código ou para a integração. Cache, fila, índice e máquina maior continuam disponíveis. Não são decisões proibidas. Só não deveriam entrar antes de sabermos qual trabalho está segurando a resposta. Antes de mudar, defino a medida que precisa melhorar e a que não pode piorar. Depois do deploy, volto ao plano, às métricas e ao profiling. Se a métrica-alvo não melhora, a hipótese não ficou de pé. Se a medida de proteção piora, a solução criou outro problema. Descarto a hipótese e retorno ao ponto do fluxo em que o tempo ainda cresce. --- *Publicado originalmente no [LinkedIn](https://www.linkedin.com/pulse/maioria-dos-problemas-de-performance-backend-come%C3%A7a-perto-s-pereira-zqlae/) em 16 de julho de 2026.* --- # Derusting logic #02: Container With Most Water Source: articles/derusting-logic-container-with-most-water-en.md > LeetCode 11 in JavaScript: a neighbor-based attempt, the hypothesis review, and the two-pointer solution. After solving the first exercise of the series, I stayed on LeetCode to work on logic without asking for a ready-made solution. Challenge 011 is [Container With Most Water](https://leetcode.com/problems/container-with-most-water/). We get an array of heights and need to pick two lines that form the container with the largest area. ### How to compute the area If I pick positions `left` and `right`, the width is the distance between them. The container height is limited by the shorter of the two lines. ```text area = min(left height, right height) × distance ``` For example, with these heights: ```text [1, 8, 6, 2, 5, 4, 8, 3, 7] ``` The lines at positions `1` and `8` have heights `8` and `7`. The shorter height is `7`, and the distance between them is `7`. That combination produces an area of `49`. ### My first attempt I started with two pointers, one at each end of the array. After computing the current area, I simulated two possibilities: - advancing the left pointer; - moving the right pointer back. I computed the area of the next two pairs and picked the larger one. The main excerpt was this: ```javascript const paddingLeftArea = Math.min(heights[leftIndex + 1], heights[rigthIndex]) * (rigthIndex - leftIndex + 1); const paddingRightArea = Math.min(heights[leftIndex], heights[rigthIndex - 1]) * (rigthIndex - 1 - leftIndex); if (paddingLeftArea > paddingRightArea && paddingLeftArea > maxArea) { leftIndex += 1; } else { rigthIndex -= 1; } ``` The problem was in the hypothesis. The best local decision does not guarantee the best area over the rest of the array. I tried to guess the path by looking only at the next two moves. There was also an error in the distance formula of that draft: for a pair of positions, the width is `right - left`. ### The observation that unlocks the problem The area depends on two things: width and shorter height. When the pointers are at positions `left` and `right`, moving the pointer of the taller line cannot raise the container's minimum height. The width always shrinks, and the height limiting the area is still there. So the pointer that must advance is the one at the shorter height. It is the only move that can find a taller line and make up for the lost width. If the heights are equal, either one can advance. In the code, I chose to advance the left one when `heights[leftIndex] <= heights[rigthIndex]`. ### Two-pointer solution ```javascript /** * @param {number[]} heights * @return {number} */ var maxArea = function (heights) { let leftIndex = 0; let rigthIndex = heights.length - 1; let maxArea = 0; while (leftIndex < rigthIndex) { const minH = Math.min(heights[leftIndex], heights[rigthIndex]); const currentArea = minH * (rigthIndex - leftIndex); maxArea = Math.max(maxArea, currentArea); if (heights[leftIndex] <= heights[rigthIndex]) { leftIndex++; } else { rigthIndex--; } } return maxArea; }; ``` Each round, I compute the current pair's area, update the largest area found, and move one of the pointers. The `while` loop converges toward the center and ends. The advancing detail matters. In the version I had written, the pointers only advanced when the current area was not larger than `maxArea`. If a new maximum area was found, the same combination would be computed again, never leaving the loop. The fix was to separate the two decisions: record the area, then move the shorter-height pointer. ### The result of the attempts The LeetCode history looked like this: - JavaScript: accepted, `3 ms` and `63.6 MB`. - JavaScript: wrong answer. - JavaScript: wrong answer. - Go: accepted, `0 ms` and `9.6 MB`. - TypeScript: accepted, `3 ms` and `63.9 MB`. - Go: wrong answer. There were three wrong-answer attempts before reaching the accepted solutions in JavaScript, Go, and TypeScript. More than counting submissions, I wanted to look at the error, understand the hypothesis that failed, and try again without outsourcing all the reasoning. ### Complexity The algorithm walks the array once. On each iteration, one of the pointers advances, so time complexity is `O(n)` and space complexity is `O(1)`. The first attempt also used two pointers but did extra work comparing future possibilities. The second solution uses a property of the problem to safely discard part of the combinations. That was the exercise this time: not mistaking a choice that looks good now for a decision the problem actually lets you justify. --- *Derusting logic series #02 — Container With Most Water. Problem at [leetcode.com/problems/container-with-most-water](https://leetcode.com/problems/container-with-most-water/).* --- # Desenferrujando a lógica #02: Container With Most Water Source: articles/desenferrujando-logica-container-with-most-water-pt-br.md > LeetCode 11 em JavaScript: uma tentativa baseada nos vizinhos, a revisão da hipótese e a solução de dois ponteiros. Depois de resolver o primeiro exercício da série, continuei no LeetCode para trabalhar a lógica sem pedir uma solução pronta. O desafio 011 é o [Container With Most Water](https://leetcode.com/problems/container-with-most-water/). Recebemos um array de alturas e precisamos escolher duas linhas que formem o recipiente com a maior área. ### Como calcular a área Se escolho as posições `left` e `right`, a largura é a distância entre elas. A altura do recipiente é limitada pela menor das duas linhas. ```text área = min(altura da esquerda, altura da direita) × distância ``` Por exemplo, com estas alturas: ```text [1, 8, 6, 2, 5, 4, 8, 3, 7] ``` As linhas nas posições `1` e `8` têm alturas `8` e `7`. A menor altura é `7`, e a distância entre elas é `7`. Essa combinação produz uma área de `49`. ### Minha primeira tentativa Comecei com dois ponteiros, um em cada ponta do array. Depois de calcular a área atual, eu simulava duas possibilidades: - avançar o ponteiro da esquerda; - recuar o ponteiro da direita. Eu calculava a área dos dois próximos pares e escolhia o maior. O trecho principal era este: ```javascript const paddingLeftArea = Math.min(heights[leftIndex + 1], heights[rigthIndex]) * (rigthIndex - leftIndex + 1); const paddingRightArea = Math.min(heights[leftIndex], heights[rigthIndex - 1]) * (rigthIndex - 1 - leftIndex); if (paddingLeftArea > paddingRightArea && paddingLeftArea > maxArea) { leftIndex += 1; } else { rigthIndex -= 1; } ``` O problema estava na hipótese. A melhor decisão local não garante a melhor área no restante do array. Eu tentava adivinhar o caminho olhando apenas para os dois próximos movimentos. Também havia um erro na fórmula da distância desse rascunho: para um par de posições, a largura é `right - left`. ### A observação que destrava o problema A área depende de duas coisas: largura e menor altura. Quando os ponteiros estão nas posições `left` e `right`, mover o ponteiro da maior altura não pode aumentar a altura mínima do recipiente. A largura sempre diminui, e a altura que limita a área continua presente. Por isso, o ponteiro que deve avançar é o da menor altura. É o único movimento que pode encontrar uma linha mais alta e compensar a perda de largura. Se as alturas forem iguais, qualquer um dos dois pode avançar. No código, escolhi avançar o da esquerda quando `heights[leftIndex] <= heights[rigthIndex]`. ### Solução com dois ponteiros ```javascript /** * @param {number[]} heights * @return {number} */ var maxArea = function (heights) { let leftIndex = 0; let rigthIndex = heights.length - 1; let maxArea = 0; while (leftIndex < rigthIndex) { const minH = Math.min(heights[leftIndex], heights[rigthIndex]); const currentArea = minH * (rigthIndex - leftIndex); maxArea = Math.max(maxArea, currentArea); if (heights[leftIndex] <= heights[rigthIndex]) { leftIndex++; } else { rigthIndex--; } } return maxArea; }; ``` A cada rodada, calculo a área do par atual, atualizo a maior área encontrada e movo um dos ponteiros. O `while` se aproxima do centro e termina. O detalhe do avanço importa. Na versão que eu havia escrito, os ponteiros só avançavam quando a área atual não era maior que `maxArea`. Se uma nova área máxima fosse encontrada, a mesma combinação seria calculada de novo, sem sair do loop. A correção foi separar as duas decisões: registrar a área e, depois, mover o ponteiro da menor altura. ### O resultado das tentativas O histórico do LeetCode ficou assim: - JavaScript: aceita, `3 ms` e `63.6 MB`. - JavaScript: resposta incorreta. - JavaScript: resposta incorreta. - Go: aceita, `0 ms` e `9.6 MB`. - TypeScript: aceita, `3 ms` e `63.9 MB`. - Go: resposta incorreta. Foram três tentativas com resposta incorreta antes de chegar às soluções aceitas em JavaScript, Go e TypeScript. Mais do que contar submissões, eu queria olhar para o erro, entender a hipótese que falhou e tentar de novo sem terceirizar todo o raciocínio. ### Complexidade O algoritmo percorre o array uma vez. Em cada iteração, um dos ponteiros avança, então a complexidade de tempo é `O(n)` e a complexidade de espaço é `O(1)`. A primeira tentativa também usava dois ponteiros, mas fazia trabalho extra para comparar possibilidades futuras. A segunda solução usa uma propriedade do problema para descartar com segurança parte das combinações. Esse foi o exercício desta vez: não confundir uma escolha que parece boa agora com uma decisão que o problema realmente permite justificar. --- *Série Desenferrujando a lógica #02 — Container With Most Water. Problema em [leetcode.com/problems/container-with-most-water](https://leetcode.com/problems/container-with-most-water/).* --- # Derusting logic #01: Group Anagrams Source: articles/desenferrujando-logica-group-anagrams-en.md > LeetCode 49 in Go: from sort-based keys to 26-letter counting, and the habit of keeping thinking after the code works. After years programming, I realized my logic was rusty. I had not stopped writing code. What changed was that, little by little, I started outsourcing parts of the reasoning I used to exercise alone. I picked exercise [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/) on LeetCode. The task is to receive a list of strings and group anagrams together. ### What we need to solve Input: ```text ["eat", "tea", "tan", "ate", "nat", "bat"] ``` Possible result: ```text ["eat", "tea", "ate"] ["tan", "nat"] ["bat"] ``` Group order does not matter. ### What is an anagram? Take `eat`, `tea`, and `ate`. Each has `a` once, `e` once, and `t` once. Positions change; the count of each letter stays the same. The algorithm must turn those words into a shared representation. If all three produce the same key, I can use that key in a `map` and place them in the same group. The first problem is creating that key. ### My first answer was sorting I started using the sorted string as the key: ```text eat → aet tea → aet ate → aet ``` All three produce `aet`. #### Sort-based solution That was the first solution that came to mind. I was not trying for the leanest implementation right away. I wanted a coherent solution and to understand where it could improve. ```go func sortString(str string) string { b := []byte(str) slices.Sort(b) return string(b) } func groupAnagrams(strs []string) [][]string { mapping := make(map[string][]string) for _, str := range strs { sortedStr := sortString(str) mapping[sortedStr] = append(mapping[sortedStr], str) } result := make([][]string, 0, len(mapping)) for _, group := range mapping { result = append(result, group) } return result } ``` What happens in this code: 1. `sortString(str)` turns the string into bytes, sorts the characters, and returns a new string. 2. `sortedStr := sortString(str)` produces the key for that term. 3. `mapping[sortedStr] = append(...)` uses that key to accumulate anagrams in the same group. 4. For `eat`, `tea`, and `ate`, `sortedStr` is always `aet`. The map starts looking like this: ```text "aet" -> ["eat", "tea", "ate"] "ant" -> ["tan", "nat"] "abt" -> ["bat"] ``` I compute one key and add the word directly to the matching group. The `map` avoids comparing every string with every other. ### Where is the cost of this approach? To discover the key, I sort each string. If a string has `k` characters, that sort costs roughly `O(k log k)`. Repeating for `n` strings, the dominant part is `O(n × k log k)`. #### Why drop the sort? For `eat`, I sorted characters to reach `aet`. For `tea`, I sorted again to reach the same `aet`. Sorting works because it creates a shared representation. But the problem does not require sorting anything. To know whether two strings are anagrams, it is enough to check whether they hold the same count of each letter. ### That observation changes the solution Instead of placing letters in the same order, I can count how many times each appears. In this exercise, inputs use lowercase letters from `a` to `z`. Each string can be represented by 26 counters. `tea` and `ate` produce the same count. I do not need to rearrange any character. I just walk the string and count occurrences. For `eat`, the relevant part is: ```text a = 1 e = 1 t = 1 ``` #### Counting solution `var key [26]uint8` creates 26 slots, one per letter from `a` to `z`. `key[str[i]-'a']++` finds each character's slot and increments the counter. Then `groups[key] = append(groups[key], str)` uses the frequency vector itself as the group key. ```go func groupAnagrams(strs []string) [][]string { groups := make(map[[26]uint8][]string, len(strs)) for _, str := range strs { var key [26]uint8 for i := 0; i < len(str); i++ { key[str[i]-'a']++ } groups[key] = append(groups[key], str) } result := make([][]string, 0, len(groups)) for _, group := range groups { result = append(result, group) } return result } ``` ### What changed in complexity? - Sorting: `O(k log k)` per string and `O(n × k log k)` for `n` strings. - Counting: `O(k)` per string and `O(n × k)` for `n` strings. The improvement appeared when I realized sorting did work the problem never asked for. The main change is from `O(k log k)` to `O(k)` per string. ### The first solution was not wrong It solves the problem and, depending on context, could be enough. The habit I wanted to recover was to keep thinking after the code starts working. Find a solution, return to the problem, and ask: what work is my algorithm doing without needing to? I am not doing these exercises because LeetCode represents all software engineering work, nor to chase the most sophisticated solution. I am doing them because I noticed that using AI every day reduced how often I insist on a problem alone. I want to reserve space to exercise that again: read, try, fail, review the approach, and only then compare paths. --- *Series Derusting logic #01 — Group Anagrams. Adapted from the original carousel; problem at [leetcode.com/problems/group-anagrams](https://leetcode.com/problems/group-anagrams/).* --- # Desenoxidando la lógica #01: Group Anagrams Source: articles/desenferrujando-logica-group-anagrams-es.md > LeetCode 49 en Go: de la clave por sort al conteo de 26 letras, y el hábito de seguir pensando después de que el código funciona. Después de años programando, percibí que mi lógica estaba oxidada. Yo no había dejado de escribir código. Lo que cambió fue que, poco a poco, empecé a tercerizar partes del razonamiento que antes necesitaba ejercitar solo. Elegí el ejercicio [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/) en LeetCode. La propuesta es recibir una lista de strings y reunir los anagramas en el mismo grupo. ### Lo que necesitamos resolver Entrada: ```text ["eat", "tea", "tan", "ate", "nat", "bat"] ``` Resultado posible: ```text ["eat", "tea", "ate"] ["tan", "nat"] ["bat"] ``` El orden de los grupos no importa. ### ¿Qué es un anagrama? Toma `eat`, `tea` y `ate`. Cada una tiene `a` una vez, `e` una vez y `t` una vez. La posición cambia; la cantidad de cada letra sigue igual. El algoritmo necesita transformar esas palabras en una representación común. Si las tres producen la misma clave, puedo usar esa clave en un `map` y colocarlas en el mismo grupo. El primer problema es crear esa clave. ### Mi primera respuesta fue ordenar Empecé usando el string ordenado como clave: ```text eat → aet tea → aet ate → aet ``` Las tres producen `aet`. #### Solución usando sort Esta fue la primera solución que se me ocurrió. No intentaba la implementación más concisa de inmediato. Quería montar una solución coherente y entender dónde podría mejorar. ```go func sortString(str string) string { b := []byte(str) slices.Sort(b) return string(b) } func groupAnagrams(strs []string) [][]string { mapping := make(map[string][]string) for _, str := range strs { sortedStr := sortString(str) mapping[sortedStr] = append(mapping[sortedStr], str) } result := make([][]string, 0, len(mapping)) for _, group := range mapping { result = append(result, group) } return result } ``` Lo que ocurre en ese código: 1. `sortString(str)` transforma el string en bytes, ordena los caracteres y devuelve un nuevo string. 2. `sortedStr := sortString(str)` produce la clave de aquel término. 3. `mapping[sortedStr] = append(...)` usa esa clave para acumular los anagramas en el mismo grupo. 4. Para `eat`, `tea` y `ate`, `sortedStr` será siempre `aet`. El mapa empieza a quedar así: ```text "aet" -> ["eat", "tea", "ate"] "ant" -> ["tan", "nat"] "abt" -> ["bat"] ``` Calculo una clave y añado la palabra directamente al grupo correspondiente. El `map` evita comparar cada string con todas las demás. ### ¿Dónde está el costo de ese enfoque? Para descubrir la clave, necesito ordenar cada string. Si un string tiene `k` caracteres, esa ordenación cuesta aproximadamente `O(k log k)`. Repitiendo para `n` strings, la parte dominante queda en `O(n × k log k)`. #### ¿Por qué quitar el sort? Para `eat`, ordenaba los caracteres para llegar a `aet`. Para `tea`, hacía otra ordenación para llegar al mismo `aet`. El `sort` funciona porque crea una representación común. Solo que el problema no exige ordenar nada. Para saber si dos strings son anagramas, basta verificar si poseen la misma cantidad de cada letra. ### Esa observación cambia la solución En lugar de colocar las letras en el mismo orden, puedo contar cuántas veces aparece cada una. En este ejercicio, las entradas usan letras minúsculas de `a` a `z`. Cada string puede representarse por 26 contadores. `tea` y `ate` producen el mismo conteo. No necesito reorganizar ningún carácter. Solo recorro el string y cuento las ocurrencias. Para `eat`, la parte relevante queda: ```text a = 1 e = 1 t = 1 ``` #### Solución usando conteo `var key [26]uint8` crea 26 posiciones, una para cada letra de `a` a `z`. `key[str[i]-'a']++` encuentra la posición de cada carácter e incrementa el contador. Después, `groups[key] = append(groups[key], str)` usa el propio vector de frecuencias como clave del grupo. ```go func groupAnagrams(strs []string) [][]string { groups := make(map[[26]uint8][]string, len(strs)) for _, str := range strs { var key [26]uint8 for i := 0; i < len(str); i++ { key[str[i]-'a']++ } groups[key] = append(groups[key], str) } result := make([][]string, 0, len(groups)) for _, group := range groups { result = append(result, group) } return result } ``` ### ¿Qué cambió en complejidad? - Ordenación: `O(k log k)` por string y `O(n × k log k)` para `n` strings. - Conteo: `O(k)` por string y `O(n × k)` para `n` strings. La mejora apareció cuando percibí que la ordenación hacía un trabajo que el problema no exigía. El cambio principal es de `O(k log k)` a `O(k)` por string. ### La primera solución no estaba equivocada Resuelve el problema y, según el contexto, podría bastar. El hábito que quería recuperar era seguir pensando después de que el código empieza a funcionar. Encontrar una solución, volver al problema y preguntar: ¿qué trabajo está haciendo mi algoritmo sin necesitarlo? No hago estos ejercicios porque LeetCode represente todo el trabajo de ingeniería de software, ni para disputar la solución más sofisticada. Los hago porque percibí que usar IA todos los días redujo la cantidad de veces en que necesito insistir solo en un problema. Quiero reservar espacio para ejercitarlo de nuevo: leer, intentar, errar, revisar el enfoque y solo después comparar caminos. --- *Serie Desenoxidando la lógica #01 — Group Anagrams. Adaptado del carrusel autoral; problema en [leetcode.com/problems/group-anagrams](https://leetcode.com/problems/group-anagrams/).* --- # Desenferrujando a lógica #01: Group Anagrams Source: articles/desenferrujando-logica-group-anagrams-pt-br.md > LeetCode 49 em Go: da chave por sort à contagem de 26 letras, e o hábito de continuar pensando depois que o código funciona. Depois de anos programando, percebi que minha lógica estava enferrujada. Eu não tinha parado de escrever código. O que mudou foi que, aos poucos, comecei a terceirizar partes do raciocínio que antes precisava exercitar sozinho. Escolhi o exercício [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/) no LeetCode. A proposta é receber uma lista de strings e reunir os anagramas no mesmo grupo. ### O que precisamos resolver Entrada: ```text ["eat", "tea", "tan", "ate", "nat", "bat"] ``` Resultado possível: ```text ["eat", "tea", "ate"] ["tan", "nat"] ["bat"] ``` A ordem dos grupos não importa. ### O que é um anagrama? Pegue `eat`, `tea` e `ate`. Cada uma tem `a` uma vez, `e` uma vez e `t` uma vez. A posição muda; a quantidade de cada letra continua igual. O algoritmo precisa transformar essas palavras em uma representação comum. Se as três produzirem a mesma chave, posso usar essa chave em um `map` e colocá-las no mesmo grupo. O primeiro problema é criar essa chave. ### Minha primeira resposta foi ordenar Comecei usando a string ordenada como chave: ```text eat → aet tea → aet ate → aet ``` As três produzem `aet`. #### Solução usando sort Essa foi a primeira solução que me ocorreu. Eu não estava tentando a implementação mais enxuta de imediato. Queria montar uma solução coerente e entender onde ela poderia melhorar. ```go func sortString(str string) string { b := []byte(str) slices.Sort(b) return string(b) } func groupAnagrams(strs []string) [][]string { mapping := make(map[string][]string) for _, str := range strs { sortedStr := sortString(str) mapping[sortedStr] = append(mapping[sortedStr], str) } result := make([][]string, 0, len(mapping)) for _, group := range mapping { result = append(result, group) } return result } ``` O que acontece nesse código: 1. `sortString(str)` transforma a string em bytes, ordena os caracteres e devolve uma nova string. 2. `sortedStr := sortString(str)` produz a chave daquele termo. 3. `mapping[sortedStr] = append(...)` usa essa chave para acumular os anagramas no mesmo grupo. 4. Para `eat`, `tea` e `ate`, `sortedStr` será sempre `aet`. O mapa começa a ficar assim: ```text "aet" -> ["eat", "tea", "ate"] "ant" -> ["tan", "nat"] "abt" -> ["bat"] ``` Calculo uma chave e adiciono a palavra diretamente ao grupo correspondente. O `map` evita comparar cada string com todas as outras. ### Onde está o custo dessa abordagem? Para descobrir a chave, preciso ordenar cada string. Se uma string tem `k` caracteres, essa ordenação custa aproximadamente `O(k log k)`. Repetindo para `n` strings, a parte dominante fica em `O(n × k log k)`. #### Por que retirar o sort? Para `eat`, eu ordenava os caracteres para chegar em `aet`. Para `tea`, fazia outra ordenação para chegar no mesmo `aet`. O `sort` funciona porque cria uma representação comum. Só que o problema não exige ordenar nada. Para saber se duas strings são anagramas, basta verificar se possuem a mesma quantidade de cada letra. ### Essa observação muda a solução Em vez de colocar as letras na mesma ordem, posso contar quantas vezes cada uma aparece. Nesse exercício, as entradas usam letras minúsculas de `a` até `z`. Cada string pode ser representada por 26 contadores. `tea` e `ate` produzem a mesma contagem. Não preciso reorganizar nenhum caractere. Só percorro a string e conto as ocorrências. Para `eat`, a parte relevante fica: ```text a = 1 e = 1 t = 1 ``` #### Solução usando contagem `var key [26]uint8` cria 26 posições, uma para cada letra de `a` até `z`. `key[str[i]-'a']++` encontra a posição de cada caractere e incrementa o contador. Depois, `groups[key] = append(groups[key], str)` usa o próprio vetor de frequências como chave do grupo. ```go func groupAnagrams(strs []string) [][]string { groups := make(map[[26]uint8][]string, len(strs)) for _, str := range strs { var key [26]uint8 for i := 0; i < len(str); i++ { key[str[i]-'a']++ } groups[key] = append(groups[key], str) } result := make([][]string, 0, len(groups)) for _, group := range groups { result = append(result, group) } return result } ``` ### O que mudou em complexidade? - Ordenação: `O(k log k)` por string e `O(n × k log k)` para `n` strings. - Contagem: `O(k)` por string e `O(n × k)` para `n` strings. A melhoria apareceu quando percebi que a ordenação fazia um trabalho que o problema não exigia. A mudança principal é de `O(k log k)` para `O(k)` por string. ### A primeira solução não estava errada Ela resolve o problema e, dependendo do contexto, poderia ser suficiente. O hábito que eu queria recuperar era continuar pensando depois que o código começa a funcionar. Encontrar uma solução, voltar ao problema e perguntar: que trabalho meu algoritmo está fazendo sem precisar? Não estou fazendo esses exercícios porque LeetCode representa todo o trabalho de engenharia de software, nem para disputar a solução mais sofisticada. Estou fazendo porque percebi que usar IA todos os dias reduziu a quantidade de vezes em que preciso insistir sozinho em um problema. Quero reservar espaço para exercitar isso novamente: ler, tentar, errar, revisar a abordagem e só depois comparar caminhos. --- *Série Desenferrujando a lógica #01 — Group Anagrams. Adaptado do carousel autoral; problema em [leetcode.com/problems/group-anagrams](https://leetcode.com/problems/group-anagrams/).* --- # Desenoxidando la lógica #02: Container With Most Water Source: articles/desenoxidando-logica-container-with-most-water-es.md > LeetCode 11 en JavaScript: un intento basado en los vecinos, la revisión de la hipótesis y la solución de dos punteros. Después de resolver el primer ejercicio de la serie, seguí en LeetCode para trabajar la lógica sin pedir una solución lista. El desafío 011 es [Container With Most Water](https://leetcode.com/problems/container-with-most-water/). Recibimos un array de alturas y necesitamos elegir dos líneas que formen el recipiente con el área mayor. ### Cómo calcular el área Si elijo las posiciones `left` y `right`, el ancho es la distancia entre ellas. La altura del recipiente está limitada por la menor de las dos líneas. ```text área = min(altura izquierda, altura derecha) × distancia ``` Por ejemplo, con estas alturas: ```text [1, 8, 6, 2, 5, 4, 8, 3, 7] ``` Las líneas en las posiciones `1` y `8` tienen alturas `8` y `7`. La menor altura es `7`, y la distancia entre ellas es `7`. Esa combinación produce un área de `49`. ### Mi primer intento Empecé con dos punteros, uno en cada punta del array. Después de calcular el área actual, simulaba dos posibilidades: - avanzar el puntero de la izquierda; - retroceder el puntero de la derecha. Calculaba el área de los dos próximos pares y elegía la mayor. El tramo principal era este: ```javascript const paddingLeftArea = Math.min(heights[leftIndex + 1], heights[rigthIndex]) * (rigthIndex - leftIndex + 1); const paddingRightArea = Math.min(heights[leftIndex], heights[rigthIndex - 1]) * (rigthIndex - 1 - leftIndex); if (paddingLeftArea > paddingRightArea && paddingLeftArea > maxArea) { leftIndex += 1; } else { rigthIndex -= 1; } ``` El problema estaba en la hipótesis. La mejor decisión local no garantiza la mejor área en el resto del array. Intentaba adivinar el camino mirando solo los dos próximos movimientos. También había un error en la fórmula de la distancia de ese borrador: para un par de posiciones, el ancho es `right - left`. ### La observación que destraba el problema El área depende de dos cosas: ancho y menor altura. Cuando los punteros están en las posiciones `left` y `right`, mover el puntero de la mayor altura no puede aumentar la altura mínima del recipiente. El ancho siempre disminuye, y la altura que limita el área sigue presente. Por eso, el puntero que debe avanzar es el de la menor altura. Es el único movimiento que puede encontrar una línea más alta y compensar la pérdida de ancho. Si las alturas son iguales, cualquiera de los dos puede avanzar. En el código, elegí avanzar el de la izquierda cuando `heights[leftIndex] <= heights[rigthIndex]`. ### Solución con dos punteros ```javascript /** * @param {number[]} heights * @return {number} */ var maxArea = function (heights) { let leftIndex = 0; let rigthIndex = heights.length - 1; let maxArea = 0; while (leftIndex < rigthIndex) { const minH = Math.min(heights[leftIndex], heights[rigthIndex]); const currentArea = minH * (rigthIndex - leftIndex); maxArea = Math.max(maxArea, currentArea); if (heights[leftIndex] <= heights[rigthIndex]) { leftIndex++; } else { rigthIndex--; } } return maxArea; }; ``` En cada ronda, calculo el área del par actual, actualizo la mayor área encontrada y muevo uno de los punteros. El `while` se aproxima al centro y termina. El detalle del avance importa. En la versión que yo había escrito, los punteros solo avanzaban cuando el área actual no era mayor que `maxArea`. Si se encontraba una nueva área máxima, la misma combinación se calculaba de nuevo, sin salir del loop. La corrección fue separar las dos decisiones: registrar el área y, después, mover el puntero de la menor altura. ### El resultado de los intentos El historial de LeetCode quedó así: - JavaScript: aceptada, `3 ms` y `63.6 MB`. - JavaScript: respuesta incorrecta. - JavaScript: respuesta incorrecta. - Go: aceptada, `0 ms` y `9.6 MB`. - TypeScript: aceptada, `3 ms` y `63.9 MB`. - Go: respuesta incorrecta. Fueron tres intentos con respuesta incorrecta antes de llegar a las soluciones aceptadas en JavaScript, Go y TypeScript. Más que contar envíos, yo quería mirar el error, entender la hipótesis que falló e intentarlo de nuevo sin tercerizar todo el razonamiento. ### Complejidad El algoritmo recorre el array una vez. En cada iteración, uno de los punteros avanza, entonces la complejidad de tiempo es `O(n)` y la complejidad de espacio es `O(1)`. El primer intento también usaba dos punteros, pero hacía trabajo extra para comparar posibilidades futuras. La segunda solución usa una propiedad del problema para descartar con seguridad parte de las combinaciones. Ese fue el ejercicio esta vez: no confundir una elección que parece buena ahora con una decisión que el problema realmente permite justificar. --- *Serie Desenoxidando la lógica #02 — Container With Most Water. Problema en [leetcode.com/problems/container-with-most-water](https://leetcode.com/problems/container-with-most-water/).* --- # Stop being hostage to dependencies. Say hello to the Adapter design pattern Source: articles/design-patterns-adapter-en.md > With the Adapter, the service depends on a protocol and adapters translate MySQL, PostgreSQL, or mocks — without coupling business rules to the driver. With the Adapter pattern, we isolate the business rule from the concrete dependency. ### Starting scenario We have a backend with a simple user CRUD: create, edit, retrieve, and delete through API endpoints. Data goes to some database — say MySQL — and the structure starts as **Controller → Service → Database**. So far, the team is comfortable. Until someone decides: next week we migrate from MySQL to PostgreSQL. From there, the house falls down on technology. Beyond restructuring the database, the team must hunt down MySQL references: inserts, connection, scattered queries. Most of the time this delays delivery, reduces quality, skips tests, and introduces bugs. It would be better to reduce the dependency between the business rule and whoever performs the specific operation — here, the database. The Adapter helps build the system that way. ### Adapter pattern We have a third-party plugin, library, module, or service that does something we want in the business rule — here, persisting data. The path: 1. Define an interface with the contract of what we need. 2. Expose only methods that make sense in context (SOLID). 3. Implement the interface in classes that adapt the third-party code. We create `CreateDatabaseCustomerProtocol` with a `create` method that receives `CustomerInputEntity` and returns `SuccessfulEntityCreation`: ```typescript interface CustomerInputEntity { name: string; email: string; birthDate: Date; } interface SuccessfulEntityCreation { readonly id: number; readonly name: string; readonly email: string; readonly birthDate: Date; } interface CreateDatabaseCustomerProtocol { createCustomerOnDatabase( customer: CustomerInputEntity, ): SuccessfulEntityCreation; } ``` Instead of **Controller → Service → Database**, we move to **Controller → Service → Protocols → Plugin**. The service loses knowledge of how the CRUD reaches the database. It is composed of protocols; the concrete implementation arrives at runtime — via dependency injection. While we use PostgreSQL, we implement the protocols in adapters (or connectors). `CreateDatabaseCustomerProtocol` can be implemented by `CreateDatabaseCustomerPostgresqlAdapter`, `CreateDatabaseCustomerMysqlAdapter`, `CreateDatabaseCustomerMongoDBAdapter`, or `CreateDatabaseCustomerMockedAdapter`. The service looks like this: ```typescript class CustomerService { constructor( private readonly createCustomer: CreateDatabaseCustomerProtocol, ) {} public register(customer: CustomerInputEntity): SuccessfulEntityCreation { return this.createCustomer.createCustomerOnDatabase(customer); } } ``` For the service, it does not matter whether the database returns JSON, XML, or another format — the adapter translates to the contract the business rule expects. ### Advantages - **Maintenance:** any plugin can be replaced without rewriting the service. - **Tests:** to test only the business rule, inject a mock adapter implementing the same protocol. - **Clean code:** separated responsibilities; the business rule does not carry driver details. ### Next step Take a frontend with dozens of libraries and identify what you actually use. Pick one feature — converting BRL to USD, for example. Describe the contract (input and output) and implement an adapter on top of the library that does it today. Repeat where the dependency hurts. ### Relation to other patterns In the article [Design Patterns: Strategy](/en/articles/design-patterns-strategy/), the focus is swapping algorithms behind a contract. The Adapter isolates external dependencies behind an owned interface. The two complement each other: Strategy varies behavior; Adapter translates the outside world. --- *Originally published on [LinkedIn](https://www.linkedin.com/pulse/pare-de-ser-ref%C3%A9m-das-depend%C3%AAncias-diga-bem-vindo-ao-design-rafael/) on April 26, 2022.* --- # Deja de ser rehén de las dependencias. Saluda al patrón de diseño Adapter Source: articles/design-patterns-adapter-es.md > Con el Adapter, el servicio depende de un protocolo y los adapters traducen MySQL, PostgreSQL o mocks — sin acoplar la regla de negocio al driver. Con el patrón Adapter, aislamos la regla de negocio de la dependencia concreta. ### Escenario inicial Tenemos un backend con CRUD simple de usuario: crear, editar, recuperar y eliminar por endpoints en la API. Los datos van a una base cualquiera — digamos MySQL — y la estructura nace como **Controller → Service → Database**. Hasta aquí, el equipo está cómodo. Hasta que alguien decide: la semana que viene migramos de MySQL a PostgreSQL. A partir de ahí la casa se cae para tecnología. Además de reestructurar la base, el equipo necesita cazar referencias a MySQL: inserciones, conexión, queries esparcidas. La mayoría de las veces esto retrasa la entrega, reduce la calidad, salta pruebas e introduce bugs. Sería mejor disminuir la dependencia entre la regla de negocio y quien ejecuta la operación específica — en este caso, la base. El Adapter ayuda a construir el sistema así. ### Patrón Adapter Tenemos un plugin, library, module o servicio de terceros que hace algo que queremos en la regla de negocio — aquí, persistir datos. El camino: 1. Definir una interfaz con el contrato de lo que necesitamos. 2. Exponer solo métodos que tengan sentido en el contexto (SOLID). 3. Implementar la interfaz en clases que adaptan el código de terceros. Creamos `CreateDatabaseCustomerProtocol` con un método `create` que recibe `CustomerInputEntity` y retorna `SuccessfulEntityCreation`: ```typescript interface CustomerInputEntity { name: string; email: string; birthDate: Date; } interface SuccessfulEntityCreation { readonly id: number; readonly name: string; readonly email: string; readonly birthDate: Date; } interface CreateDatabaseCustomerProtocol { createCustomerOnDatabase( customer: CustomerInputEntity, ): SuccessfulEntityCreation; } ``` En lugar de **Controller → Service → Database**, pasamos a **Controller → Service → Protocols → Plugin**. El servicio pierde el conocimiento de cómo el CRUD llega a la base. Está compuesto por protocolos; la implementación concreta entra en tiempo de ejecución — por inyección de dependencias. Mientras usamos PostgreSQL, implementamos los protocolos en los adapters (o connectors). `CreateDatabaseCustomerProtocol` puede ser implementado por `CreateDatabaseCustomerPostgresqlAdapter`, `CreateDatabaseCustomerMysqlAdapter`, `CreateDatabaseCustomerMongoDBAdapter` o `CreateDatabaseCustomerMockedAdapter`. El servicio queda así: ```typescript class CustomerService { constructor( private readonly createCustomer: CreateDatabaseCustomerProtocol, ) {} public register(customer: CustomerInputEntity): SuccessfulEntityCreation { return this.createCustomer.createCustomerOnDatabase(customer); } } ``` Para el servicio, da igual si la base devuelve JSON, XML u otro formato — el adapter traduce al contrato que la regla de negocio espera. ### Ventajas - **Mantenimiento:** cualquier plugin puede sustituirse sin reescribir el servicio. - **Pruebas:** para probar solo la regla de negocio, inyecta un adapter mock que implemente el mismo protocolo. - **Código limpio:** responsabilidades separadas; la regla de negocio no carga detalles del driver. ### Próximo paso Toma un frontend con decenas de bibliotecas e identifica lo que realmente usas. Elige una funcionalidad — convertir reales a dólares, por ejemplo. Describe el contrato (entrada y salida) e implementa un adapter sobre la biblioteca que hoy lo hace. Repite donde la dependencia moleste. ### Relación con otros patrones En el artículo [Design Patterns: Strategy](/es/articulos/design-patterns-strategy/), el foco es intercambiar algoritmos tras un contrato. El Adapter aísla dependencias externas tras una interfaz propia. Ambos se complementan: Strategy varía comportamiento; Adapter traduce el mundo exterior. --- *Publicado originalmente en [LinkedIn](https://www.linkedin.com/pulse/pare-de-ser-ref%C3%A9m-das-depend%C3%AAncias-diga-bem-vindo-ao-design-rafael/) el 26 de abril de 2022.* --- # Pare de ser refém das dependências. Diga olá ao Design Pattern Adapter Source: articles/design-patterns-adapter-pt-br.md > Com o Adapter, o serviço depende de um protocolo e os adapters traduzem MySQL, PostgreSQL ou mocks — sem acoplar a regra de negócio ao driver. Com o padrão Adapter, isolamos a regra de negócio da dependência concreta. ### Cenário inicial Temos um backend com CRUD simples de usuário: criar, editar, recuperar e deletar por endpoints na API. Os dados vão para um banco qualquer — digamos MySQL — e a estrutura nasce como **Controller → Service → Database**. Até aqui, o time está confortável. Até alguém decidir: na semana que vem migramos de MySQL para PostgreSQL. A partir daí a casa cai para tecnologia. Além de reestruturar o banco, o time precisa caçar referências ao MySQL: inserções, conexão, queries espalhadas. Na maioria das vezes isso atrasa entrega, reduz qualidade, pula testes e introduz bugs. Seria melhor diminuir a dependência entre a regra de negócio e quem executa a operação específica — no caso, o banco. O Adapter ajuda a construir o sistema assim. ### Padrão Adapter Temos um plugin, library, module ou serviço de terceiros que faz algo que queremos na regra de negócio — aqui, persistir dados. O caminho: 1. Definir uma interface com o contrato do que precisamos. 2. Expor só métodos que fazem sentido no contexto (SOLID). 3. Implementar a interface em classes que adaptam o código de terceiros. Criamos `CreateDatabaseCustomerProtocol` com um método `create` que recebe `CustomerInputEntity` e retorna `SuccessfulEntityCreation`: ```typescript interface CustomerInputEntity { name: string; email: string; birthDate: Date; } interface SuccessfulEntityCreation { readonly id: number; readonly name: string; readonly email: string; readonly birthDate: Date; } interface CreateDatabaseCustomerProtocol { createCustomerOnDatabase( customer: CustomerInputEntity, ): SuccessfulEntityCreation; } ``` Em vez de **Controller → Service → Database**, passamos a **Controller → Service → Protocols → Plugin**. O serviço perde o conhecimento de como o CRUD chega ao banco. Ele é composto pelos protocolos; a implementação concreta entra em tempo de execução — por injeção de dependência. Enquanto usamos PostgreSQL, implementamos os protocolos nos adapters (ou connectors). `CreateDatabaseCustomerProtocol` pode ser implementado por `CreateDatabaseCustomerPostgresqlAdapter`, `CreateDatabaseCustomerMysqlAdapter`, `CreateDatabaseCustomerMongoDBAdapter` ou `CreateDatabaseCustomerMockedAdapter`. O serviço fica assim: ```typescript class CustomerService { constructor( private readonly createCustomer: CreateDatabaseCustomerProtocol, ) {} public register(customer: CustomerInputEntity): SuccessfulEntityCreation { return this.createCustomer.createCustomerOnDatabase(customer); } } ``` Para o serviço, tanto faz se o banco devolve JSON, XML ou outro formato — o adapter traduz para o contrato que a regra de negócio espera. ### Vantagens - **Manutenção:** qualquer plugin pode ser substituído sem reescrever o serviço. - **Testes:** para testar só a regra de negócio, injete um adapter mock que implementa o mesmo protocolo. - **Código limpo:** responsabilidades separadas; a regra de negócio não carrega detalhes do driver. ### Próximo passo Pegue um frontend com dezenas de bibliotecas e identifique o que você realmente usa. Escolha uma funcionalidade — converter real em dólar, por exemplo. Descreva o contrato (entrada e saída) e implemente um adapter em cima da biblioteca que hoje faz isso. Repita onde a dependência incomoda. ### Relação com outros padrões No artigo [Design Patterns: Strategy](/artigos/design-patterns-strategy/), o foco é trocar algoritmos atrás de um contrato. O Adapter isola dependências externas atrás de uma interface própria. Os dois se complementam: Strategy varia comportamento; Adapter traduz o mundo de fora. --- *Publicado originalmente no [LinkedIn](https://www.linkedin.com/pulse/pare-de-ser-ref%C3%A9m-das-depend%C3%AAncias-diga-bem-vindo-ao-design-rafael/) em 26 de abril de 2022.* --- # Design Patterns: Strategy Source: articles/design-patterns-strategy-en.md > How the Strategy pattern encapsulates interchangeable algorithms and avoids fragile if/else chains, with a TypeScript calculator example. `if/else` chains grow and turn fragile. The Strategy pattern encapsulates each algorithm in its own class and lets implementations be swapped at runtime without changing the consuming code. ### The problem: endless if/else A calculator with addition, subtraction, multiplication, and division usually starts as a `DefaultCalculator` class: private methods per operation and a public function choosing which to invoke with a `switch` or `if/else` chain. ```typescript class DefaultCalculator { public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; switch (operator) { case "*": return firstOperand * secondOperand; case "+": return firstOperand + secondOperand; case "-": return firstOperand - secondOperand; case "/": return firstOperand / secondOperand; case "**": return firstOperand ** secondOperand; case "%": return firstOperand % secondOperand; default: throw new Error("Operator not found!"); } } } ``` The problem appears when the calculator must cover more binary operations between integers: percent, exponentiation, modulo, bit shifts. Each new feature changes the original implementation, raises coupling, and makes maintenance more expensive. ### What is the Strategy pattern? The pattern defines functionality through a contract (interface), implemented according to context. The interface defines the operation; concrete implementations define its execution. Consuming code depends on the abstraction. Each strategy stays isolated in its own class. New behaviors arrive without changing existing code, aligned with the Open/Closed Principle. ### Defining the contract The first step is the strategy contract interface. For the calculator, something receiving two numbers and returning the result: ```typescript interface BinaryOperationParameters { firstOperand: number; secondOperand: number; operator: string; } type Result = number; interface BinaryOperationStrategy { calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result; } ``` ### Implementing concrete strategies Each math operation becomes a class implementing `BinaryOperationStrategy` and executing a single operation. Addition and division: ```typescript class Sum implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; return firstOperand + secondOperand; } } class Division implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; if (secondOperand === 0) { throw new Error("Division by zero is not allowed!"); } return firstOperand / secondOperand; } } ``` ### The Context and the Factory To tie strategies together, a Context and a Factory (or Analyzer) come in. In `ContextAnalyzer`, a method evaluates the operator and returns the right Strategy: ```typescript class ContextAnalyzer { public getInstance(operator: string): BinaryOperationStrategy { switch (operator) { case "*": return new Multiplication(); case "+": return new Sum(); case "-": return new Subtraction(); case "/": return new Division(); case "%": return new Percent(); case "**": return new Pow(); default: throw new Error("Operator not found!"); } } } ``` The context receives and executes the strategy. It knows only the contract, not the concrete implementation. The old `DefaultCalculator` starts receiving this `ContextAnalyzer` by injection: ```typescript class Calculator { constructor(private readonly contextAnalyzer: ContextAnalyzer) {} /** * The calculate implementation in Calculator does not change per operation. * What grows is the ContextAnalyzer, which adds one case per new operation. */ public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; return this.contextAnalyzer .getInstance(operator) .calculate({ firstOperand, secondOperand }); } } ``` ### Why use Strategy? Strategy helps in legacy code with several business rules, each represented by an `if` and a long implementation. The shared part stays in the contract; each rule variation stays in its own class; a context analyzer (resolver/factory) picks the strategy. On each request, the code evaluates the operation context and selects the implementation matching the contract. ### Relation to other patterns - Adapter: Strategy varies behavior; Adapter isolates external dependencies behind an owned interface. - SOLID (OCP): Strategy is one way to apply the Open/Closed Principle. --- # Design Patterns: Strategy Source: articles/design-patterns-strategy-es.md > Cómo el patrón Strategy encapsula algoritmos intercambiables y evita frágiles cadenas de if/else, con un ejemplo de calculadora en TypeScript. Las cadenas de `if/else` crecen y se vuelven frágiles. El patrón Strategy encapsula cada algoritmo en su propia clase y permite intercambiar implementaciones en tiempo de ejecución sin alterar el código que las consume. ### El problema: if/else infinito Una calculadora con suma, resta, multiplicación y división suele nacer como una clase `DefaultCalculator`: métodos privados por operación y una función pública que elige cuál invocar con `switch` o cadena de `if/else`. ```typescript class DefaultCalculator { public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; switch (operator) { case "*": return firstOperand * secondOperand; case "+": return firstOperand + secondOperand; case "-": return firstOperand - secondOperand; case "/": return firstOperand / secondOperand; case "**": return firstOperand ** secondOperand; case "%": return firstOperand % secondOperand; default: throw new Error("Operator not found!"); } } } ``` El problema aparece cuando la calculadora necesita cubrir más operaciones binarias entre enteros: porcentaje, exponenciación, módulo, shift de bits. Cada funcionalidad nueva altera la implementación original, sube el acoplamiento y encarece el mantenimiento. ### ¿Qué es el patrón Strategy? El patrón define la funcionalidad por medio de un contrato (interfaz), implementado según el contexto. La interfaz define la operación; las implementaciones concretas definen su ejecución. El código consumidor depende de la abstracción. Cada estrategia queda aislada en su propia clase. Nuevos comportamientos entran sin alterar el código existente, alineado al Open/Closed Principle. ### Definiendo el contrato El primer paso es la interfaz del contrato de la estrategia. En la calculadora, algo que reciba dos números y devuelva el resultado: ```typescript interface BinaryOperationParameters { firstOperand: number; secondOperand: number; operator: string; } type Result = number; interface BinaryOperationStrategy { calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result; } ``` ### Implementando estrategias concretas Cada operación matemática se vuelve una clase que implementa `BinaryOperationStrategy` y ejecuta una única operación. Suma y división: ```typescript class Sum implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; return firstOperand + secondOperand; } } class Division implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; if (secondOperand === 0) { throw new Error("Division by zero is not allowed!"); } return firstOperand / secondOperand; } } ``` ### El Context y la Factory Para amarrar las estrategias, entran un Context y una Factory (o Analyzer). En `ContextAnalyzer`, un método evalúa el operador y retorna la Strategy correcta: ```typescript class ContextAnalyzer { public getInstance(operator: string): BinaryOperationStrategy { switch (operator) { case "*": return new Multiplication(); case "+": return new Sum(); case "-": return new Subtraction(); case "/": return new Division(); case "%": return new Percent(); case "**": return new Pow(); default: throw new Error("Operator not found!"); } } } ``` El contexto recibe y ejecuta la estrategia. Conoce solo el contrato, no la implementación concreta. La antigua `DefaultCalculator` pasa a recibir ese `ContextAnalyzer` por inyección: ```typescript class Calculator { constructor(private readonly contextAnalyzer: ContextAnalyzer) {} /** * La implementación de calculate en Calculator no cambia por operación. * Lo que crece es el ContextAnalyzer, que añade un case por operación nueva. */ public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; return this.contextAnalyzer .getInstance(operator) .calculate({ firstOperand, secondOperand }); } } ``` ### ¿Por qué usar Strategy? Strategy ayuda en legado con varias reglas de negocio, cada una representada por un `if` y una implementación extensa. La parte común queda en el contrato; cada variación de regla queda en su propia clase; un analizador de contexto (resolver/factory) elige la estrategia. En cada petición, el código evalúa el contexto de la operación y selecciona la implementación correspondiente al contrato. ### Relación con otros patrones - Adapter: Strategy varía comportamiento; Adapter aísla dependencias externas tras una interfaz propia. - SOLID (OCP): Strategy es una forma de aplicar el Open/Closed Principle. --- # Design Patterns: Strategy Source: articles/design-patterns-strategy-pt-br.md > Como o padrão Strategy encapsula algoritmos intercambiáveis e evita cadeias frágeis de if/else, com um exemplo de calculadora em TypeScript. Cadeias de `if/else` crescem e ficam frágeis. O padrão Strategy encapsula cada algoritmo em sua própria classe e permite trocar implementações em tempo de execução sem alterar o código que as consome. ### O problema: if/else infinito Uma calculadora com soma, subtração, multiplicação e divisão costuma nascer como uma classe `DefaultCalculator`: métodos privados por operação e uma função pública que escolhe qual invocar com `switch` ou cadeia de `if/else`. ```typescript class DefaultCalculator { public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; switch (operator) { case "*": return firstOperand * secondOperand; case "+": return firstOperand + secondOperand; case "-": return firstOperand - secondOperand; case "/": return firstOperand / secondOperand; case "**": return firstOperand ** secondOperand; case "%": return firstOperand % secondOperand; default: throw new Error("Operator not found!"); } } } ``` O problema aparece quando a calculadora precisa cobrir mais operações binárias entre inteiros: percentual, exponenciação, módulo, shift de bits. Cada funcionalidade nova altera a implementação original, sobe o acoplamento e encarece a manutenção. ### O que é o padrão Strategy? O padrão define a funcionalidade por meio de um contrato (interface), implementado conforme o contexto. A interface define a operação; as implementações concretas definem sua execução. O código consumidor depende da abstração. Cada estratégia fica isolada em sua própria classe. Novos comportamentos entram sem alterar o código existente, alinhado ao Open/Closed Principle. ### Definindo o contrato O primeiro passo é a interface do contrato da estratégia. Na calculadora, algo que receba dois números e retorne o resultado: ```typescript interface BinaryOperationParameters { firstOperand: number; secondOperand: number; operator: string; } type Result = number; interface BinaryOperationStrategy { calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result; } ``` ### Implementando estratégias concretas Cada operação matemática vira uma classe que implementa `BinaryOperationStrategy` e executa uma única operação. Soma e divisão: ```typescript class Sum implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; return firstOperand + secondOperand; } } class Division implements BinaryOperationStrategy { public calculate( parameters: Pick< BinaryOperationParameters, "firstOperand" | "secondOperand" >, ): Result { const { firstOperand, secondOperand } = parameters; if (secondOperand === 0) { throw new Error("Division by zero is not allowed!"); } return firstOperand / secondOperand; } } ``` ### O Context e a Factory Para amarrar as estratégias, entram um Context e uma Factory (ou Analyzer). Em `ContextAnalyzer`, um método avalia o operador e retorna a Strategy correta: ```typescript class ContextAnalyzer { public getInstance(operator: string): BinaryOperationStrategy { switch (operator) { case "*": return new Multiplication(); case "+": return new Sum(); case "-": return new Subtraction(); case "/": return new Division(); case "%": return new Percent(); case "**": return new Pow(); default: throw new Error("Operator not found!"); } } } ``` O contexto recebe e executa a estratégia. Ele conhece apenas o contrato, não a implementação concreta. A antiga `DefaultCalculator` passa a receber esse `ContextAnalyzer` por injeção: ```typescript class Calculator { constructor(private readonly contextAnalyzer: ContextAnalyzer) {} /** * A implementação de calculate em Calculator não muda a cada operação. * O que cresce é o ContextAnalyzer, que adiciona um case por operação nova. */ public calculate(parameters: BinaryOperationParameters): Result { const { operator, firstOperand, secondOperand } = parameters; return this.contextAnalyzer .getInstance(operator) .calculate({ firstOperand, secondOperand }); } } ``` ### Por que usar o Strategy? O Strategy ajuda em legado com várias regras de negócio, cada uma representada por um `if` e uma implementação extensa. A parte comum fica no contrato; cada variação de regra fica em sua própria classe; um analisador de contexto (resolver/factory) escolhe a estratégia. A cada requisição, o código avalia o contexto da operação e seleciona a implementação correspondente ao contrato. ### Relação com outros padrões - Adapter: o Strategy varia comportamento; o Adapter isola dependências externas atrás de uma interface própria. - SOLID (OCP): o Strategy é uma forma de aplicar o Open/Closed Principle. --- # Go intensive: concurrency, resilience, and distributed systems in 30 minutes Source: articles/go-intensive-en.md > A practical Go backend review covering goroutines, context, backpressure, idempotency, Kubernetes, observability, and performance. This is a review for backend engineers who already know Go and need to discuss or build production services again. The useful decisions are bounded concurrency, cancellation, finite queues, idempotency, and observability. The scope is Go 1.26. It is not a language introduction. Move quickly through syntax and spend time on the choices that shape a consumer, API, or telemetry pipeline. ### 30-minute route | Time | Topic | Priority | | --- | --- | --- | | 0-4 min | Types, structs, interfaces, errors | Quick review | | 4-10 min | Goroutines, channels, `select`, context | High | | 10-17 min | Bounded concurrency and backpressure | Highest | | 17-23 min | Resilient IoT pipeline | Highest | | 23-26 min | Runtime, memory, profiling | High | | 26-30 min | Architecture and interview answers | Highest | ### Production fundamentals Go favors composition, small contracts, and explicit flow. A value can validate itself without a framework: ```go var ErrOutOfRange = errors.New("reading out of range") type Reading struct { DeviceID string Sequence uint64 Value float64 } func (r Reading) Validate() error { if r.DeviceID == "" { return errors.New("device_id is required") } if r.Value < -100 || r.Value > 250 { return fmt.Errorf("%w: %.2f", ErrOutOfRange, r.Value) } return nil } ``` The zero value is often useful. Slices share a backing array until `append` reallocates; maps have no iteration order and need synchronization for concurrent access. Strings are immutable bytes, usually UTF-8. `defer` runs in LIFO order, while evaluating its arguments when registered. Interfaces are satisfied implicitly. Define a small interface where it is consumed. Errors are values: add context with `%w` and inspect the chain with `errors.Is` or `errors.As`. Reserve `panic` for broken invariants or unrecoverable startup failures. ### Goroutines, channels, and context A goroutine is not a dedicated OS thread. The runtime schedules it over OS threads. Every goroutine needs a clear owner, stop condition, and wait path. Channels move work or ownership. Mutexes protect shared state. A buffered channel smooths a temporary speed difference; it does not create unlimited capacity. ```go func enqueue(ctx context.Context, jobs chan<- Reading, reading Reading) error { select { case jobs <- reading: return nil case <-ctx.Done(): return context.Cause(ctx) } } ``` The producer closes a channel when it knows no more values will be sent. Sending to a closed channel or closing it twice panics. A `nil` channel blocks forever, and disables its `select` case. `context.Context` carries cancellation, deadlines, and request-scoped metadata. Receive it first, propagate it, call every returned `cancel`, and do not store it in a struct. Cancellation is cooperative: blocking loops must watch `ctx.Done()`. ### Bound concurrency before memory becomes the limit One goroutine per message becomes expensive when a downstream slows down. Queues and heap grow, GC gets busier, and the process may fail before CPU looks full. `errgroup` combines waiting, first-error propagation, and shared cancellation. Set a limit for independent tasks: ```go func ProcessBatch(ctx context.Context, batch []Reading) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(16) for _, reading := range batch { reading := reading g.Go(func() error { return processOne(ctx, reading) }) } return g.Wait() } ``` Classify errors before acting. A database outage can cancel a batch. Invalid, duplicate, or schema-incompatible messages belong in quarantine or a DLQ, not in a failure that stops the whole consumer. Use `atomic` for an independent flag or counter, `sync.Mutex` for an invariant across fields, and a channel for work transfer. Do not copy a mutex after first use or keep a lock during remote I/O. Backpressure is a product and operations policy. When ingestion accepts 50,000 messages per second and persistence completes 20,000, storing the difference in memory only moves the incident. Decide whether to block producers, reject with retry, pause consumption so the broker holds durable backlog, discard stale samples, aggregate, or spill to disk. Define queue size, occupancy metric, timeout, and saturation action. ### A resilient IoT pipeline ```text device -> MQTT/broker -> Go ingestion -> stream -> processors -> storage \-> DLQ \-> current state ``` MQTT fits device connectivity. A stream such as Kafka fits durable retention, replay, and internal partitioning. gRPC is typed internal RPC; WebSocket updates dashboards. These protocols solve different boundaries. Multiple workers break global ordering. Telemetry commonly needs order per device, so partition by a stable key such as `hash(device_id) % N` and process each partition sequentially. Keep both `observed_at` and `ingested_at`, plus `sequence`, `event_id`, and `boot_id` where applicable. Device clocks drift and restart. Treat the end-to-end path as *at least once*. Receive the event, validate its envelope and schema version, check an idempotency key, persist the effect and deduplication marker in one transaction where possible, then ACK. A transactional outbox closes the gap between committing database state and publishing a following event. Consumers still need idempotency because duplicates remain possible. Retry only transient failures. Invalid payloads and rejected business rules do not improve with another attempt. Add a limit, a total budget, and jitter so replicas do not retry together. At the edge, use TLS, per-device identities, topic authorization, strict payload limits, and validation before allocating large structures. Rotation, revocation, sequence numbers, nonces, and time windows matter when the business protocol must resist replay. ### Kubernetes, observability, and performance On `SIGTERM`, remove readiness, stop fetching work, drain in-flight work within the grace period, ACK only completed messages, and close producers, connections, and telemetry last. Liveness asks whether the process progresses; it should not depend on every external service. Readiness asks whether this pod can accept work now. For consumer autoscaling, CPU alone is weak. Watch lag, age of the oldest message, arrival rate, processing time, and worker-pool occupancy. Use structured logs and correlation fields without logging credentials or full sensitive payloads. Track throughput, errors by class, p50/p95/p99, lag, event age, retries, DLQ volume, duplicates, goroutines, heap, and GC pauses. Use sampled traces across ingestion, stream, and persistence; tracing every high-frequency reading can cost more than it helps. A data race is concurrent access to one memory location with at least one write and no synchronization order. Channel sends, mutex unlock/lock, and atomic operations create useful ordering. The detector only covers executed paths: ```bash go test -race ./... go test -bench=. -benchmem ./... go tool pprof cpu.out go tool trace trace.out ``` G is a goroutine, M an OS thread, and P a logical execution resource. `GOMAXPROCS` limits Ps that run Go code concurrently, not the goroutine count. Goroutines are lightweight, not free. Profile before pooling or micro-optimizing. Preallocate known slice capacity, avoid repeated `string`/`[]byte` conversions in hot paths, and treat `sync.Pool` as an opportunistic temporary-object cache. ### Interview answers to keep ready **Is a goroutine a thread?** No. It is a lightweight runtime-managed execution unit multiplexed over OS threads. **Channel or mutex?** Use a channel to transfer work or ownership; use a mutex to protect shared state and invariants. **Who closes a channel?** The producer that knows there will be no more sends. **How do you preserve ordering with workers?** Avoid global ordering unless it is required. Partition by a key such as `device_id` and process each partition sequentially. **How do you handle duplicates?** Use a stable idempotency key, transactional deduplication where possible, and naturally idempotent updates such as an upsert with version or sequence. **How do you investigate latency?** Separate queue time, processing, and dependencies. Compare p95/p99, lag, and saturation, then test a hypothesis with tracing, `pprof`, or `go tool trace`. ### Production checklist - Does every goroutine have an owner and a stop condition? - Where do context cancellation and deadlines propagate? - What is the concurrency limit and what happens at saturation? - Is ordering required per key or globally? - When does ACK happen, and how does the mutation survive re-delivery? - Which failures retry, go to DLQ, or go to quarantine? - How do device time and ingestion time differ? - Which metrics expose lag, p99, heap, GC, and contention? ### Official references - [Go 1.26 Release Notes](https://go.dev/doc/go1.26) - [The Go Memory Model](https://go.dev/ref/mem) - [Go Concurrency Patterns: Context](https://go.dev/blog/context) - [Go Concurrency Patterns: Pipelines and cancellation](https://go.dev/blog/pipelines) - [Package errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) - [Data Race Detector](https://go.dev/doc/articles/race_detector) - [Diagnostics and profiling](https://go.dev/doc/diagnostics) - [Package log/slog](https://pkg.go.dev/log/slog) - [Kubernetes probes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes) --- # Intensivo de Golang: concurrencia, resiliencia y sistemas distribuidos en 30 minutos Source: articles/go-intensivo-es.md > Revisión práctica de Go para backend: goroutines, context, backpressure, idempotencia, Kubernetes, observabilidad y rendimiento. Esta guía es para quien ya trabaja con backend y necesita volver a conversar o implementar servicios Go de producción. Las decisiones que más importan son concurrencia limitada, cancelación, colas finitas, idempotencia y observabilidad. El recorte usa Go 1.26. No es una introducción al lenguaje. Repasa la sintaxis rápido y dedica el tiempo a las decisiones que cambian el diseño de un consumer, una API o un pipeline de telemetría. ### Ruta de 30 minutos | Tiempo | Bloque | Prioridad | | --- | --- | --- | | 0-4 min | Tipos, structs, interfaces y errores | Revisión rápida | | 4-10 min | Goroutines, channels, `select` y contexto | Alta | | 10-17 min | Concurrencia limitada y backpressure | Máxima | | 17-23 min | Pipeline IoT resiliente | Máxima | | 23-26 min | Runtime, memoria y profiling | Alta | | 26-30 min | Arquitectura y entrevista | Máxima | ### Fundamentos que aparecen en producción Go favorece composición, contratos pequeños y flujo explícito. Un valor puede llevar su propia validación sin depender de un framework: ```go var ErrOutOfRange = errors.New("reading out of range") type Reading struct { DeviceID string Sequence uint64 Value float64 } func (r Reading) Validate() error { if r.DeviceID == "" { return errors.New("device_id is required") } if r.Value < -100 || r.Value > 250 { return fmt.Errorf("%w: %.2f", ErrOutOfRange, r.Value) } return nil } ``` El zero value suele ser útil. Los slices comparten backing array hasta que `append` realoca; los maps no tienen orden de iteración y necesitan sincronización para acceso concurrente. Un `string` contiene bytes inmutables, generalmente UTF-8. `defer` se ejecuta en orden LIFO, pero evalúa sus argumentos al registrarse. Las interfaces se satisfacen implícitamente. Define interfaces pequeñas donde se consumen. Los errores son valores: añade contexto con `%w` e inspecciona la cadena con `errors.Is` o `errors.As`. Reserva `panic` para invariantes rotas o fallos irrecuperables de arranque. ### Goroutines, channels y contexto Una goroutine no es un hilo dedicado. El runtime la planifica sobre hilos del sistema operativo. Cada goroutine necesita owner, condición de término y alguien que espere su finalización. Los channels transfieren trabajo u ownership. Los mutexes protegen estado compartido. Un channel con buffer suaviza una diferencia temporal de velocidad; no crea capacidad infinita. ```go func enqueue(ctx context.Context, jobs chan<- Reading, reading Reading) error { select { case jobs <- reading: return nil case <-ctx.Done(): return context.Cause(ctx) } } ``` El productor cierra un channel cuando sabe que no habrá más envíos. Enviar a un channel cerrado o cerrarlo dos veces causa `panic`. Un channel `nil` bloquea para siempre y deshabilita su caso dentro de `select`. `context.Context` propaga cancelación, deadlines y metadatos de la request. Recíbelo primero, propágalo, llama a cada `cancel` retornado y no lo guardes en una struct. La cancelación es cooperativa: los loops bloqueantes deben observar `ctx.Done()`. ### Limita la concurrencia antes de que la memoria sea el límite Una goroutine por mensaje se vuelve costosa cuando un downstream se ralentiza. Crecen las colas y el heap, el GC trabaja más y el proceso puede caer antes de que la CPU parezca llena. `errgroup` combina espera, propagación del primer error y cancelación compartida. Pon un límite para tareas independientes: ```go func ProcessBatch(ctx context.Context, batch []Reading) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(16) for _, reading := range batch { reading := reading g.Go(func() error { return processOne(ctx, reading) }) } return g.Wait() } ``` Clasifica los errores antes de actuar. Una base indisponible puede cancelar un lote. Un payload inválido, duplicado o incompatible con el schema debe ir a cuarentena o DLQ, sin detener el consumer completo. Usa `atomic` para un contador o flag independiente, `sync.Mutex` para una invariante entre campos y channel para transferir trabajo. No copies un mutex después del primer uso ni mantengas un lock durante I/O remoto. Backpressure es una política de producto y operación. Si la ingesta recibe 50 mil mensajes por segundo y la persistencia completa 20 mil, guardar el resto en memoria solo desplaza el incidente. Decide si bloquear productor, rechazar con retry, pausar consumo para que el broker retenga backlog durable, descartar muestras antiguas, agregar datos o persistir en disco. Define tamaño de cola, métrica de ocupación, timeout y acción de saturación. ### Pipeline IoT resistente a reentregas ```text dispositivo -> MQTT/broker -> ingesta Go -> stream -> procesadores -> almacenamiento \-> DLQ \-> estado actual ``` MQTT encaja en la conectividad de dispositivos. Un stream como Kafka encaja en retención durable, replay y particionamiento interno. gRPC es RPC interno tipado; WebSocket actualiza dashboards. Resuelven fronteras distintas. Varios workers rompen el orden global. Telemetría suele necesitar orden por dispositivo, así que particiona por una clave estable como `hash(device_id) % N` y procesa cada partición de forma secuencial. Guarda `observed_at`, `ingested_at`, `sequence`, `event_id` y `boot_id` cuando exista. El reloj del dispositivo puede desfasarse o reiniciarse. Diseña la cadena para entrega *at least once*. Recibe el evento, valida envelope y versión del schema, comprueba la clave de idempotencia, persiste efecto y marcador de deduplicación en la misma transacción cuando sea posible y recién entonces hace ACK. Una transactional outbox cierra la ventana entre confirmar el estado en base y publicar el evento siguiente. El consumer sigue necesitando idempotencia porque las duplicatas pueden ocurrir. Retry sirve para fallos transitorios. Un payload inválido o una regla de negocio rechazada no mejora con otro intento. Añade límite, budget total y jitter para que las réplicas no repitan juntas. En el borde usa TLS, identidad por dispositivo, autorización por tópico, límite estricto de payload y validación antes de asignar estructuras grandes. Rotación, revocación, secuencia, nonce y ventana temporal importan cuando el protocolo debe resistir replay. ### Kubernetes, observabilidad y rendimiento En `SIGTERM`, quita readiness, deja de buscar trabajo, drena el trabajo en vuelo dentro del grace period, confirma solo los mensajes concluidos y cierra productores, conexiones y telemetría al final. Liveness pregunta si el proceso progresa y no debe depender de cada servicio externo. Readiness pregunta si ese pod puede aceptar trabajo ahora. Para escalar consumers, CPU por sí sola es una señal débil. Observa lag, edad del mensaje más antiguo, tasa de llegada, tiempo de procesamiento y ocupación del pool. Usa logs estructurados y campos de correlación sin registrar credenciales ni payloads sensibles completos. Mide throughput, errores por clase, p50/p95/p99, lag, edad del evento, retries, DLQ, duplicatas, goroutines, heap y pausas de GC. Usa tracing muestreado para cruzar ingesta, stream y persistencia; trazar cada lectura de alta frecuencia puede costar más de lo que ayuda. Una data race es acceso concurrente a la misma posición de memoria con al menos una escritura y sin orden de sincronización. Envíos por channel, unlock/lock de mutex y operaciones atómicas establecen relaciones de orden. El detector solo cubre rutas ejecutadas: ```bash go test -race ./... go test -bench=. -benchmem ./... go tool pprof cpu.out go tool trace trace.out ``` G es goroutine, M es hilo del sistema y P es recurso lógico de ejecución. `GOMAXPROCS` limita cuántos Ps ejecutan código Go en paralelo, no cuántas goroutines pueden existir. Las goroutines son ligeras, no gratuitas. Mide antes de usar pools u optimizar microdetalles. Preasigna capacidad conocida para slices, evita conversiones repetidas entre `string` y `[]byte` en hot paths y trata `sync.Pool` como cache oportunista de temporales. ### Respuestas para entrevista **¿Una goroutine es un hilo?** No. Es una unidad ligera gestionada por el runtime y multiplexada sobre hilos del sistema operativo. **¿Channel o mutex?** Channel para transferir trabajo u ownership; mutex para proteger estado compartido e invariantes. **¿Quién cierra un channel?** El productor que sabe que no habrá más envíos. **¿Cómo preservar orden con workers?** Evita orden global si no es requisito. Particiona por una clave como `device_id` y procesa cada partición secuencialmente. **¿Cómo manejas duplicatas?** Con una clave de idempotencia estable, deduplicación transaccional cuando sea posible y operaciones idempotentes, como upsert con versión o secuencia. **¿Cómo investigas latencia?** Separa tiempo de fila, procesamiento y dependencias. Compara p95/p99, lag y saturación; después prueba una hipótesis con tracing, `pprof` o `go tool trace`. ### Checklist de producción - ¿Cada goroutine tiene owner y condición de término? - ¿Dónde se propagan cancelación y deadline del contexto? - ¿Cuál es el límite de concurrencia y qué pasa en saturación? - ¿El orden necesario es por clave o global? - ¿Cuándo ocurre ACK y cómo la mutación resiste reentrega? - ¿Qué fallos hacen retry, van a DLQ o a cuarentena? - ¿Cómo se diferencian tiempo del dispositivo y tiempo de ingesta? - ¿Qué métricas exponen lag, p99, heap, GC y contención? ### Referencias oficiales - [Go 1.26 Release Notes](https://go.dev/doc/go1.26) - [The Go Memory Model](https://go.dev/ref/mem) - [Go Concurrency Patterns: Context](https://go.dev/blog/context) - [Go Concurrency Patterns: Pipelines and cancellation](https://go.dev/blog/pipelines) - [Package errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) - [Data Race Detector](https://go.dev/doc/articles/race_detector) - [Diagnostics and profiling](https://go.dev/doc/diagnostics) - [Package log/slog](https://pkg.go.dev/log/slog) - [Kubernetes probes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes) --- # Goroutines vs Event Loop: the wrong comparison between two concurrency models Source: articles/goroutines-vs-event-loop-en.md > Concurrency is not parallelism. When the Node.js Event Loop is enough for I/O and when Go goroutines fit CPU-bound load better. The thesis is simple: Node.js with the Event Loop and Go with goroutines do not solve the same kind of problem the same way. The comparison goes bad when we treat both as direct competitors in every scenario. In practice, the most common mistake is confusing concurrency with parallelism. Concurrency is organizing several tasks that may be underway at the same time. Parallelism is actually executing work at the same time, using multiple CPU cores. That difference sounds academic until it shows up in production. The Node.js Event Loop is very good when the bottleneck is waiting: external APIs, databases, WebSockets, user input, queues, and events. While one operation waits for a response, the loop keeps serving other tasks. It is the corner-store owner at the counter: he does not stop because he asked someone to fetch candy from the stockroom. Goroutines, on the other hand, start looking more interesting when the work is CPU bound, divisible, and can use multiple cores with explicit concurrency control. They are lightweight execution units managed by the Go runtime. With them, a task can be broken into smaller parts, distributed, and synchronized at the end. It is more like a busy stall at the São João festival in Caruaru: one person grills corn, another stirs canjica, another slices bolo de rolo. Work advances at the same time, each person handling one part. ### Where Node.js starts to suffer I saw this very concretely in a commission-calculation process at a betting company. The application handled millions of bets per day, and part of the flow computed commission over several bet batches. At first, the Node.js process worked. Sequentially it was correct, but slow. When I tried to parallelize with the usual logic of several tasks at once, the limit appeared: the bottleneck was CPU. It was not just waiting on database, API, or external events. It was computation over a large bet buffer. That is the kind of scenario where `Promise.all` can mislead. It gives a sense of parallelism, but it does not automatically turn heavy CPU work into real parallel execution. If the tasks are compute-intensive and run on the same main thread, the Event Loop stays busy. The result can be worse than expected: loop blocking, higher latency, worse responsiveness, and more pressure on CPU and memory. The problem was not Node.js being bad. The problem was using the default Node model for a load that demanded another kind of execution. ### Where Go fit better The solution was rewriting that process in Go with goroutines. The idea was to split the calculation into smaller chunks, process those pieces in parallel, and synchronize only at the end. That design fit the problem better because the work was CPU bound and divisible. Instead of a centralized flow trying to coordinate several heavy operations, processing became distributed across smaller execution units. With a worker pool, for example, you can control the goroutine count, limit fan-out, use available cores better, and avoid firing unbounded work. The gain showed up. Total time dropped about 25%. The process that sat around 30 seconds started running near 22.5 seconds. CPU usage also improved. But the important part of the story is not "Go fixed it". The important part is that Go fixed one side of the problem and revealed another. ### The bottleneck can move The first difficulty was guaranteeing the Go result equaled the Node.js result. That is less glamorous than talking about concurrency, but it is what separates real optimization from masked regression. If the calculation gets faster and changes the financial result, the improvement is worthless. After that, the main problem became chunk splitting. The initial strategy consumed too much memory. In local tests, with smaller datasets, proportional growth reached near 15% at some points. The discomfort came from a wrong expectation: I thought moving to Go would automatically solve the problem. In practice, I had only moved the bottleneck. Before, the limit was clearer on CPU. After, the partitioning strategy started pressuring memory. That can happen for several reasons: unnecessary copies, large buffers, slices holding references to bigger arrays, oversized internal queues, or too much work prepared before being processed. In the final result, memory still grew about 5%. In that case, the time and CPU gain compensated the loss. But that is not a universal rule. If the production load were much larger, or if the service ran with a thin memory margin, that trade-off could stop being acceptable. Parallelism costs coordination, allocation, synchronization, and observability. There is no free parallel execution. ### When I would keep Node.js I would keep Node.js without hesitation for I/O orchestration: WebSocket communication, calls to several APIs, database queries, event dispatch, integration between services, and flows where dead time is waiting. In those cases, the Event Loop is an excellent choice. It allows high volumes of concurrent operations without creating one thread per request. For event-oriented applications, that is simple, productive, and easy to fit into the JavaScript ecosystem. The mistake is pushing that same model onto heavy computation and thinking I/O concurrency becomes CPU parallelism. It does not. ### When I would look at Go I would start looking at Go when the task is clearly CPU bound: high-volume data computation, batch image processing, heavy aggregations, compression, large buffer transforms, simulations, or any routine where the machine spends more time computing than waiting for external responses. In that kind of scenario, goroutines with a worker pool give better control over CPU use. They also make the separation between work units, synchronization, and result collection more explicit. But Go also charges a price. You must think about chunk granularity, memory consumption, cancellation, error handling, backpressure, worker limits, contention, and result consistency. If the work split is naive, the CPU gain can arrive with memory blowups or needless complexity. ### Counterargument: Node.js also has worker threads There is a fair counterargument: Node.js is not limited to the Event Loop for everything. Worker threads exist precisely to run heavy work outside the main thread. There are also strategies with queues, separate processes, helper services, and native addons. So the honest comparison is not "Node.js cannot". It can. The question is implementation cost, team maturity, observability, integration with the existing system, and how much effort is worth investing to keep that processing inside the Node ecosystem. On some teams, worker threads may be enough and cheaper than introducing Go. On others, splitting CPU-bound processing into a Go service may be simpler to operate and scale. The decision should not come from language preference. It should come from the nature of the load. ### The practical rule that stuck After that case, the rule that stuck for me is this: if the problem is waiting on many things at once, Node.js with the Event Loop tends to orchestrate very well. If the problem is computing many things at once, I consider Go with goroutines earlier. But I also grew suspicious of migrations promising to fix everything. Switching technology may only move the bottleneck. In my case, CPU and time improved, but memory and chunking had to be reanalyzed. In the end, the goroutines vs Event Loop fight is less about which model is superior and more about which bottleneck you are trying to attack. For I/O, the corner-store counter works very well. For CPU, sometimes you really need more people in the festival kitchen. --- *Originally published on [LinkedIn](https://www.linkedin.com/pulse/goroutines-vs-event-loop-compara%C3%A7%C3%A3o-errada-entre-dois-s-pereira-qtgle/) on June 24, 2026.* --- # Goroutines vs Event Loop: la comparación equivocada entre dos modelos de concurrencia Source: articles/goroutines-vs-event-loop-es.md > Concurrencia no es paralelismo. Cuándo el Event Loop de Node.js basta para I/O y cuándo las goroutines en Go encajan mejor en carga CPU bound. La tesis es simple: Node.js con Event Loop y Go con goroutines no resuelven el mismo tipo de problema del mismo modo. La comparación se vuelve mala cuando tratamos ambos como competidores directos en cualquier escenario. En la práctica, el error más común es confundir concurrencia con paralelismo. Concurrencia es organizar varias tareas que pueden estar en curso al mismo tiempo. Paralelismo es ejecutar trabajo de hecho al mismo tiempo, usando múltiples núcleos de CPU. Esa diferencia parece académica hasta que aparece en producción. El Event Loop de Node.js es muy bueno cuando el cuello de botella está en la espera: API externa, base de datos, WebSocket, input de usuario, colas y eventos. Mientras una operación aguarda respuesta, el loop sigue atendiendo otras tareas. Es el dueño de la bodega en el mostrador: no para porque pidió a alguien buscar rapadura en el depósito. Las goroutines, por otro lado, empiezan a volverse más interesantes cuando el trabajo es CPU bound, divisible y puede aprovechar múltiples núcleos con control explícito de concurrencia. Son unidades ligeras de ejecución gestionadas por el runtime de Go. Con ellas, se puede romper una tarea en partes menores, distribuir la ejecución y sincronizar el resultado al final. Es más parecido a un puesto lleno en el São João de Caruaru: una persona asa el maíz, otra revuelve la canjica, otra corta el bolo de rolo. El trabajo avanza al mismo tiempo, con cada persona cuidando una parte. ### El punto donde Node.js empieza a sufrir Lo vi de forma muy concreta en un proceso de cálculo de comisión en una casa de apuestas. La aplicación lidiaba con millones de apuestas por día, y parte del flujo implicaba calcular comisión sobre varios lotes de apuestas. Al principio, el proceso en Node.js funcionaba. Secuencialmente era correcto, pero lento. Cuando intenté paralelizar con la lógica común de varias tareas al mismo tiempo, apareció el límite: el cuello de botella era CPU. No era solo esperar base, API o evento externo. Era cálculo sobre un buffer grande de apuestas. Ese es el tipo de escenario en que `Promise.all` puede engañar. Da sensación de paralelismo, pero no transforma automáticamente trabajo pesado de CPU en ejecución paralela real. Si las tareas son cómputo intenso y corren en el mismo hilo principal, el Event Loop queda ocupado. El resultado puede ser peor de lo esperado: bloqueo del loop, aumento de latencia, peor responsividad y mayor presión sobre CPU y memoria. El problema no era que Node.js fuera malo. El problema era usar el modelo estándar de Node para una carga que exigía otro tipo de ejecución. ### Donde Go encajó mejor La solución fue reescribir ese proceso en Go usando goroutines. La idea era dividir el cálculo en chunks menores, procesar esos pedazos en paralelo y sincronizar solo al final. Ese diseño encajaba mejor en el problema porque el trabajo era CPU bound y podía dividirse. En lugar de un flujo centralizado intentando coordinar varias operaciones pesadas, el procesamiento pasó a distribuirse en unidades menores de ejecución. Con un worker pool, por ejemplo, se puede controlar el número de goroutines, limitar el fan-out, usar mejor los cores disponibles y evitar que el sistema dispare trabajo sin límite. La ganancia apareció. El tiempo total cayó cerca del 25%. El proceso que quedaba en torno a los 30 segundos pasó a correr en algo cercano a 22,5 segundos. También hubo mejor uso de CPU. Pero la parte importante de la historia no es "Go lo resolvió". La parte importante es que Go resolvió un lado del problema y reveló otro. ### El cuello de botella puede cambiar de lugar La primera dificultad fue garantizar que el resultado en Go fuera igual al resultado en Node.js. Esto es menos glamoroso que hablar de concurrencia, pero es lo que separa la optimización real de la regresión enmascarada. Si el cálculo se vuelve más rápido y cambia el resultado financiero, la mejora no vale nada. Después de eso, el problema principal se volvió la división de los chunks. La estrategia inicial consumía demasiada memoria. En pruebas locales, con datasets menores, el crecimiento proporcional llegó cerca del 15% en algunos momentos. La incomodidad venía de una expectativa equivocada: pensé que cambiar a Go resolvería automáticamente el problema. En la práctica, solo había movido el cuello de botella. Antes el límite estaba más claro en CPU. Después, la estrategia de particionamiento empezó a presionar la memoria. Esto puede pasar por varios motivos: copias innecesarias, buffers grandes, slices manteniendo referencia a arrays mayores, colas internas demasiado grandes o exceso de trabajo preparado antes de procesarse. En el resultado final, la memoria aún creció cerca del 5%. En ese caso, la ganancia de tiempo y CPU compensó la pérdida. Pero esto no es una regla universal. Si la carga de producción fuera mucho mayor, o si el servicio corriera con poco margen de memoria, ese intercambio podría dejar de ser aceptable. El paralelismo cuesta coordinación, asignación, sincronización y observabilidad. No existe ejecución paralela gratis. ### Cuándo mantendría Node.js Mantendría Node.js sin incomodidad para orquestación de I/O: comunicación por WebSocket, llamadas a varias APIs, consultas en base, disparo de eventos, integración entre servicios y flujos donde el tiempo muerto está en la espera. En esos casos, el Event Loop es una excelente elección. Permite alto volumen de operaciones concurrentes sin crear un hilo por request. Para aplicaciones orientadas a eventos, esto es simple, productivo y fácil de encajar en el ecosistema JavaScript. El error es intentar empujar ese mismo modelo hacia un cálculo pesado y pensar que la concurrencia de I/O se vuelve paralelismo de CPU. No se vuelve. ### Cuándo miraría hacia Go Empezaría a mirar hacia Go cuando la tarea fuera claramente CPU bound: cálculo en alto volumen de datos, procesamiento de imagen en lote, agregaciones pesadas, compresión, transformación grande de buffers, simulaciones o cualquier rutina en que la máquina pase más tiempo calculando que esperando respuesta externa. En ese tipo de escenario, las goroutines con worker pool dan mejor control sobre el uso de CPU. También dejan más explícita la separación entre unidades de trabajo, sincronización y recolección de resultado. Pero Go también cobra precio. Hay que pensar en granularidad de los chunks, consumo de memoria, cancelación, tratamiento de error, backpressure, límites de workers, contención y consistencia del resultado. Si la división del trabajo es ingenua, la ganancia de CPU puede venir acompañada de estallido de memoria o complejidad innecesaria. ### Contraargumento: Node.js también tiene worker threads Existe un contraargumento justo: Node.js no está limitado al Event Loop para todo. Los worker threads existen justamente para ejecutar trabajo pesado fuera del hilo principal. También hay estrategias con colas, procesos separados, servicios auxiliares y native addons. Entonces la comparación honesta no es "Node.js no puede". Puede. La cuestión es costo de implementación, madurez del equipo, observabilidad, integración con el sistema existente y cuánto esfuerzo vale invertir para mantener ese procesamiento dentro del ecosistema Node. En algunos equipos, usar worker threads puede bastar y ser más barato que introducir Go. En otros, separar el procesamiento CPU bound en un servicio Go puede ser más simple de operar y escalar. La decisión no debería nacer de preferencia por lenguaje. Debería nacer de la naturaleza de la carga. ### La regla práctica que quedó Después de ese caso, la regla que me quedó es esta: si el problema es esperar muchas cosas al mismo tiempo, Node.js con Event Loop tiende a orquestar muy bien. Si el problema es calcular muchas cosas al mismo tiempo, considero Go con goroutines más temprano. Pero también pasé a desconfiar de la migración que promete resolverlo todo. Cambiar tecnología puede solo desplazar el cuello de botella. En mi caso, mejoró CPU y tiempo, pero obligó a reanalizar memoria y chunking. Al final, la pelea entre goroutines y Event Loop es menos sobre qué modelo es superior y más sobre qué cuello de botella intentas atacar. Para I/O, el mostrador de la bodega funciona muy bien. Para CPU, a veces necesitas más gente en la cocina de la fiesta. --- *Publicado originalmente en [LinkedIn](https://www.linkedin.com/pulse/goroutines-vs-event-loop-compara%C3%A7%C3%A3o-errada-entre-dois-s-pereira-qtgle/) el 24 de junio de 2026.* --- # Goroutines vs Event Loop: a comparação errada entre dois modelos de concorrência Source: articles/goroutines-vs-event-loop-pt-br.md > Concorrência não é paralelismo. Quando o Event Loop do Node.js basta para I/O e quando goroutines em Go encaixam melhor em carga CPU bound. A tese é simples: Node.js com Event Loop e Go com goroutines não resolvem o mesmo tipo de problema do mesmo jeito. A comparação fica ruim quando a gente trata os dois como concorrentes diretos em qualquer cenário. Na prática, o erro mais comum é confundir concorrência com paralelismo. Concorrência é organizar várias tarefas que podem estar em andamento ao mesmo tempo. Paralelismo é executar trabalho de fato ao mesmo tempo, usando múltiplos núcleos de CPU. Essa diferença parece acadêmica até aparecer em produção. O Event Loop do Node.js é muito bom quando o gargalo está em espera: API externa, banco de dados, WebSocket, input de usuário, filas e eventos. Enquanto uma operação aguarda resposta, o loop continua atendendo outras tarefas. É o dono da bodega no balcão: ele não para porque pediu a alguém para buscar a rapadura no estoque. Goroutines, por outro lado, começam a ficar mais interessantes quando o trabalho é CPU bound, divisível e pode aproveitar múltiplos núcleos com controle explícito de concorrência. Elas são unidades leves de execução gerenciadas pelo runtime do Go. Com elas, dá para quebrar uma tarefa em partes menores, distribuir a execução e sincronizar o resultado no final. É mais parecido com uma barraca cheia no São João de Caruaru: uma pessoa assa o milho, outra mexe a canjica, outra corta o bolo de rolo. O trabalho avança ao mesmo tempo, com cada pessoa cuidando de uma parte. ### O ponto onde o Node.js começa a sofrer Eu vi isso de forma bem concreta em um processo de cálculo de comissão em uma casa de apostas. A aplicação lidava com milhões de apostas por dia, e parte do fluxo envolvia calcular comissão sobre vários lotes de apostas. No começo, o processo em Node.js funcionava. Sequencialmente era correto, mas lento. Quando tentei paralelizar com a lógica comum de várias tarefas ao mesmo tempo, o limite apareceu: o gargalo era CPU. Não era só esperar banco, API ou evento externo. Era cálculo em cima de um buffer grande de apostas. Esse é o tipo de cenário em que `Promise.all` pode enganar. Ele passa a sensação de paralelismo, mas não transforma automaticamente trabalho pesado de CPU em execução paralela real. Se as tarefas são computação intensa e rodam no mesmo thread principal, o Event Loop fica ocupado. O resultado pode ser pior do que o esperado: bloqueio do loop, aumento de latência, pior responsividade e maior pressão sobre CPU e memória. O problema não era Node.js ser ruim. O problema era usar o modelo padrão do Node para uma carga que exigia outro tipo de execução. ### Onde Go entrou melhor A solução foi reescrever esse processo em Go usando goroutines. A ideia era dividir o cálculo em chunks menores, processar esses pedaços em paralelo e sincronizar apenas no final. Esse desenho encaixava melhor no problema porque o trabalho era CPU bound e podia ser dividido. Em vez de um fluxo centralizado tentando coordenar várias operações pesadas, o processamento passou a ser distribuído em unidades menores de execução. Com um worker pool, por exemplo, dá para controlar o número de goroutines, limitar o fan-out, usar melhor os cores disponíveis e evitar que o sistema dispare trabalho sem limite. O ganho apareceu. O tempo total caiu cerca de 25%. O processo que ficava na casa dos 30 segundos passou a rodar em algo próximo de 22,5 segundos. Também houve melhor uso de CPU. Mas a parte importante da história não é “Go resolveu”. A parte importante é que Go resolveu um lado do problema e revelou outro. ### O gargalo pode mudar de lugar A primeira dificuldade foi garantir que o resultado em Go fosse igual ao resultado em Node.js. Isso é menos glamouroso do que falar sobre concorrência, mas é o que separa otimização real de regressão mascarada. Se o cálculo fica mais rápido e muda o resultado financeiro, a melhoria não vale nada. Depois disso, o problema principal virou a divisão dos chunks. A estratégia inicial consumia memória demais. Em testes locais, com datasets menores, o crescimento proporcional chegou perto de 15% em alguns momentos. O incômodo vinha de uma expectativa errada: eu achei que mudar para Go automaticamente resolveria o problema. Na prática, eu só tinha movido o gargalo de lugar. Antes o limite estava mais claro na CPU. Depois, a estratégia de particionamento começou a pressionar memória. Isso pode acontecer por vários motivos: cópias desnecessárias, buffers grandes, slices mantendo referência para arrays maiores, filas internas grandes demais ou excesso de trabalho sendo preparado antes de ser processado. No resultado final, a memória ainda cresceu cerca de 5%. Nesse caso, o ganho de tempo e CPU compensou a perda. Mas isso não é uma regra universal. Se a carga de produção fosse muito maior, ou se o serviço estivesse rodando com margem pequena de memória, essa troca poderia deixar de ser aceitável. Paralelismo custa coordenação, alocação, sincronização e observabilidade. Não existe execução paralela grátis. ### Quando eu manteria Node.js Eu manteria Node.js sem incômodo para orquestração de I/O: comunicação por WebSocket, chamadas para várias APIs, consultas em banco, disparo de eventos, integração entre serviços e fluxos onde o tempo morto está na espera. Nesses casos, o Event Loop é uma excelente escolha. Ele permite alto volume de operações concorrentes sem criar uma thread por requisição. Para aplicações orientadas a evento, isso é simples, produtivo e fácil de encaixar no ecossistema JavaScript. O erro é tentar empurrar esse mesmo modelo para um cálculo pesado e achar que concorrência de I/O vira paralelismo de CPU. Não vira. ### Quando eu olharia para Go Eu começaria a olhar para Go quando a tarefa fosse claramente CPU bound: cálculo em alto volume de dados, processamento de imagem em lote, agregações pesadas, compressão, transformação grande de buffers, simulações ou qualquer rotina em que a máquina passa mais tempo calculando do que esperando resposta externa. Nesse tipo de cenário, goroutines com worker pool dão um controle melhor sobre uso de CPU. Também deixam mais explícita a separação entre unidades de trabalho, sincronização e coleta de resultado. Mas Go também cobra preço. É preciso pensar em granularidade dos chunks, consumo de memória, cancelamento, tratamento de erro, backpressure, limites de workers, contenção e consistência do resultado. Se a divisão do trabalho for ingênua, o ganho de CPU pode vir acompanhado de estouro de memória ou complexidade desnecessária. ### Contraargumento: Node.js também tem worker threads Existe um contraargumento justo: Node.js não está limitado ao Event Loop para tudo. Worker threads existem justamente para executar trabalho pesado fora do thread principal. Também há estratégias com filas, processos separados, serviços auxiliares e native addons. Então a comparação honesta não é “Node.js não consegue”. Consegue. A questão é custo de implementação, maturidade da equipe, observabilidade, integração com o sistema existente e quanto esforço vale a pena investir para manter aquele processamento dentro do ecossistema Node. Em alguns times, usar worker threads pode ser suficiente e mais barato do que introduzir Go. Em outros, separar o processamento CPU bound em um serviço Go pode ser mais simples de operar e escalar. A decisão não deveria nascer de preferência por linguagem. Deveria nascer da natureza da carga. ### A regra prática que ficou Depois desse caso, a regra que ficou para mim é esta: se o problema é esperar muita coisa ao mesmo tempo, Node.js com Event Loop tende a orquestrar muito bem. Se o problema é calcular muita coisa ao mesmo tempo, eu considero Go com goroutines mais cedo. Mas eu também passei a desconfiar de migração que promete resolver tudo. Trocar tecnologia pode só deslocar o gargalo. No meu caso, melhorou CPU e tempo, mas obrigou a reanalisar memória e chunking. No fim, a briga entre goroutines e Event Loop é menos sobre qual modelo é superior e mais sobre qual gargalo você está tentando atacar. Para I/O, o balcão da bodega funciona muito bem. Para CPU, às vezes você precisa mesmo colocar mais gente na cozinha da festa. --- *Publicado originalmente no [LinkedIn](https://www.linkedin.com/pulse/goroutines-vs-event-loop-compara%C3%A7%C3%A3o-errada-entre-dois-s-pereira-qtgle/) em 24 de junho de 2026.* --- # Intensivão Golang: concorrência, resiliência e sistemas distribuídos em 30 minutos Source: articles/intensivao-go-pt-br.md > Revisão prática de Go para backend: goroutines, context, backpressure, idempotência, Kubernetes, observabilidade e performance. Este roteiro serve para quem já trabalha com backend e quer reativar Go para uma conversa técnica ou um serviço de produção. O foco está nas decisões que mantêm um sistema previsível sob carga: concorrência limitada, cancelamento, filas finitas, idempotência e observabilidade. O recorte usa Go 1.26. Não é uma introdução à linguagem. Passe rápido pelos fundamentos e retenha os pontos que mudam o desenho de um consumer, uma API ou um pipeline de telemetria. ### Roteiro de 30 minutos | Tempo | Bloco | Prioridade | | --- | --- | --- | | 0-4 min | Tipos, structs, interfaces e erros | Revisão rápida | | 4-10 min | Goroutines, channels, `select` e contexto | Alta | | 10-17 min | Limites de concorrência e backpressure | Máxima | | 17-23 min | Pipeline IoT resiliente | Máxima | | 23-26 min | Runtime, memória e profiling | Alta | | 26-30 min | Arquitetura e perguntas de entrevista | Máxima | ### Fundamentos que aparecem em produção Go favorece composição, contratos pequenos e fluxo explícito. Não há herança de classes nem exceções como mecanismo normal de controle. Um tipo simples pode carregar sua própria validação: ```go package telemetry import ( "errors" "fmt" "time" ) var ErrOutOfRange = errors.New("reading out of range") type Reading struct { DeviceID string `json:"device_id"` Sequence uint64 `json:"sequence"` ObservedAt time.Time `json:"observed_at"` Value float64 `json:"value"` } func (r Reading) Validate() error { if r.DeviceID == "" { return errors.New("device_id is required") } if r.Value < -100 || r.Value > 250 { return fmt.Errorf("%w: %.2f", ErrOutOfRange, r.Value) } return nil } ``` Alguns detalhes evitam erros silenciosos: - O zero value costuma ser utilizável. Prefira tipos cujo estado inicial seja válido quando isso não esconder uma regra de negócio. - Slice é uma visão sobre um array. Cópias podem compartilhar o mesmo backing array; `append` pode reutilizá-lo ou alocar outro. - `map` não tem ordem de iteração e não suporta leitura e escrita concorrentes sem sincronização. - `string` contém bytes imutáveis, normalmente UTF-8. `len` conta bytes; `range` decodifica runes. - `defer` executa em LIFO, mas avalia os argumentos quando é registrado. Interfaces são satisfeitas implicitamente. Defina a interface pequena no pacote que a consome, em vez de exportar um contrato grande ao lado da implementação. Erros são valores: acrescente contexto com `%w` e inspecione a causa com `errors.Is` ou `errors.As`. ```go if err := reading.Validate(); err != nil { if errors.Is(err, ErrOutOfRange) { return sendToDLQ(reading, err) } return fmt.Errorf("validate reading: %w", err) } ``` `panic` fica para invariantes quebradas ou falha irrecuperável de inicialização. `recover` só alcança um panic na mesma goroutine e pertence a fronteiras controladas, como middleware. ### Goroutines, channels e cancelamento Uma goroutine não é uma thread dedicada. O runtime a agenda sobre threads do sistema operacional. Iniciar uma goroutine sem saber quem a cancela ou espera cria risco de leak. Channels transportam trabalho ou propriedade. Um mutex protege estado compartilhado. Um buffer apenas absorve uma diferença temporária de velocidade; não cria capacidade infinita. ```go jobs := make(chan Reading, 128) go func() { defer close(jobs) // quem produz fecha for _, reading := range batch { jobs <- reading } }() for reading := range jobs { if err := process(reading); err != nil { // tratar ou registrar o erro da mensagem } } ``` O produtor fecha o channel quando não haverá novo envio. Enviar em channel fechado ou fechá-lo duas vezes causa `panic`. Receber de um channel fechado retorna o zero value e `ok == false`. Um channel `nil` bloqueia para sempre; dentro de `select`, ele desabilita o caso. `select` combina envio, cancelamento e política de saturação. Sem `default`, a operação espera espaço ou cancelamento. Com `default`, ela rejeita imediatamente quando a fila está cheia: ```go func enqueue(ctx context.Context, jobs chan<- Reading, reading Reading) error { select { case jobs <- reading: return nil case <-ctx.Done(): return context.Cause(ctx) } } ``` `context.Context` carrega cancelamento, deadline e metadados estritamente ligados à requisição. Receba-o como primeiro argumento, propague-o, chame todo `cancel` retornado e não guarde contexto em struct. Cancelar não encerra uma goroutine à força: os loops e operações bloqueantes precisam observar `ctx.Done()`. ### Concorrência limitada antes da pressão de memória Uma goroutine por mensagem parece barata até um downstream ficar lento. A fila cresce, o heap cresce, o GC trabalha mais e o processo pode cair antes de a CPU parecer saturada. Para tarefas independentes que falham juntas, `errgroup` oferece espera, propagação do primeiro erro e cancelamento compartilhado. `SetLimit` estabelece o teto de concorrência. ```go func ProcessBatch(ctx context.Context, batch []Reading) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(16) for _, reading := range batch { reading := reading g.Go(func() error { if err := processOne(ctx, reading); err != nil { return fmt.Errorf("device %s: %w", reading.DeviceID, err) } return nil }) } return g.Wait() } ``` Classifique o erro antes de decidir o que fazer. Um banco indisponível pode justificar cancelar o lote. Um payload inválido, duplicado ou fora do schema deve ir para quarentena ou DLQ, sem derrubar o consumer inteiro. Escolha a primitiva pela propriedade que precisa preservar: | Necessidade | Primitiva | | --- | --- | | Contador ou flag independente | `atomic` tipado | | Invariante entre vários campos | `sync.Mutex` | | Leitura frequente e escrita curta | `sync.RWMutex`, depois de medir | | Transferir trabalho ou ownership | channel | | Inicialização única | `sync.Once` | | Esperar tarefas sem erro | `sync.WaitGroup` | | Esperar tarefas com erro e cancelamento | `errgroup` | Não copie mutex depois do primeiro uso e não mantenha lock durante I/O remoto. `RWMutex` não é uma melhoria automática para uma seção crítica pequena. Backpressure é uma decisão de produto e operação. Se a ingestão recebe 50 mil mensagens por segundo e a persistência confirma 20 mil, acumular o restante em memória apenas muda o incidente de lugar. | Política | Consequência | | --- | --- | | Bloquear produtor | Aumenta latência e preserva dados quando o protocolo aceita desacelerar | | Rejeitar com erro | Exige retry e idempotência no cliente | | Pausar consumo ou ACK | Mantém backlog no broker durável | | Descartar dados antigos | Preserva frescor quando histórico não importa | | Agregar ou downsample | Reduz resolução para aliviar a carga | | Persistir em disco | Evita perda, com custo operacional adicional | Defina tamanho de buffer, métrica de ocupação, timeout e ação de saturação. Buffer sem política não é estratégia de capacidade. ### Pipeline IoT que tolera reentrega Uma separação comum é: ```text dispositivo -> MQTT/broker -> ingestão Go -> stream -> processadores -> armazenamento \-> DLQ \-> estado atual ``` MQTT atende bem à borda e às conexões dos dispositivos. Um stream como Kafka atende retenção, replay e particionamento interno. gRPC é RPC interno tipado; WebSocket atende atualização de dashboards. Nenhum deles substitui os outros automaticamente. Fan-out de workers quebra ordem global. Em telemetria, o requisito costuma ser ordem por dispositivo. Particione por uma chave estável, como `hash(device_id) % N`, e processe cada partição de forma sequencial. Guarde `observed_at`, `ingested_at`, `sequence`, `event_id` e, quando existir, `boot_id`. O relógio do dispositivo pode estar errado ou reiniciar. Projete a cadeia para entrega *at least once*. Um fluxo seguro recebe o evento, valida envelope e versão, verifica a chave de idempotência, grava efeito e marcador de deduplicação na mesma transação quando possível e só então confirma a mensagem. Para publicação após uma atualização de banco, uma transactional outbox elimina a janela entre confirmar a transação e publicar o evento. Duplicatas ainda podem ocorrer, portanto o consumidor continua idempotente. Retry serve para falha transitória. Payload inválido e regra de negócio rejeitada não melhoram com novas tentativas. Use limite, budget total e jitter para evitar que todos os pods repitam juntos: ```go func retry(ctx context.Context, max int, fn func(context.Context) error) error { var err error for attempt := 0; attempt < max; attempt++ { if err = fn(ctx); err == nil { return nil } base := min(100*time.Millisecond< Aprofundamento do roteiro de 30 minutos de Go: limites de concorrência, desenho de consumers, idempotência e trade-offs de produção. Este guia é o aprofundamento do roteiro de 30 minutos de Go. Ele parte dos mesmos fundamentos — goroutines, channels, contexto, backpressure e idempotência — para discutir com mais calma quando cada decisão de desenho se aplica. Se você ainda não passou pela revisão rápida, comece pelo [roteiro de 30 minutos](/artigos/intensivao-go/) e volte aqui para aprofundar cada bloco. ### Como usar este guia Avance na ordem proposta: cada seção isola uma decisão de desenho, descreve o mecanismo, a falha que ele trata, um exemplo autocontido e o critério para escolher outra abordagem. Todos os cenários abaixo são didáticos e hipotéticos — servem para treinar leitura de trade-offs, não descrevem sistemas reais. Leia com um editor aberto e adapte os snippets ao seu próprio exercício antes de levar qualquer padrão para um sistema real. ### 1. Modelo de execução: goroutines, scheduler e GOMAXPROCS **Mecanismo.** Goroutines são unidades leves de execução multiplexadas pelo scheduler do runtime sobre threads do sistema operacional. `GOMAXPROCS` define quantas threads podem executar código Go simultaneamente; por padrão, acompanha o número de CPUs disponíveis. Desde o Go 1.14, as goroutines são assincronamente preemptíveis: o scheduler pode interrompê-las mesmo em loops apertados sem pontos explícitos de cooperação, distribuindo trabalho sem que cada tarefa exija uma thread dedicada. **Falha ou limite que ele trata.** O modelo evita o custo de uma thread por tarefa concorrente e reduz troca de contexto do sistema operacional. O limite aparece quando se confunde concorrência com paralelismo: criar milhares de goroutines bloqueadas em I/O lento é barato, mas criar milhares de goroutines em loop apertado de CPU com `GOMAXPROCS` baixo apenas serializa o trabalho e aumenta pressão sobre o escalonador e o coletor de lixo. **Exemplo de aplicação.** Cenário didático: processar uma lista de itens independentes em paralelo, limitada ao número de CPUs. ```go package main import ( "fmt" "runtime" "sync" ) func process(item int) int { // Simula transformação pura de CPU. return item * item } func main() { items := []int{1, 2, 3, 4, 5, 6, 7, 8} results := make([]int, len(items)) numWorkers := runtime.GOMAXPROCS(0) jobs := make(chan int) var wg sync.WaitGroup for w := 0; w < numWorkers; w++ { wg.Add(1) go func() { defer wg.Done() for index := range jobs { results[index] = process(items[index]) } }() } for i := range items { jobs <- i } close(jobs) wg.Wait() fmt.Println(results) } ``` **Quando escolher outra abordagem.** Mantenha o valor default de `GOMAXPROCS`; se considerar sobrescrevê-lo, meça antes e depois em benchmarks controlados. Para tarefas puramente sequenciais, dependentes entre si ou com overhead de coordenação maior que o ganho, o laço simples sem goroutines é mais legível e mais rápido. Para paralelismo de dados em lote com cancelamento e limite de erro, prefira `errgroup` ou um pool com semáforo em vez de disparar goroutines sem controle. ### 2. Ownership e cancelamento com context **Mecanismo.** `context.Context` propaga cancelamento, deadline e valores de escopo de requisição ao longo de uma cadeia de chamadas. O dono do contexto (normalmente a borda de entrada: handler HTTP, consumidor de fila, função `main`) cria um contexto cancelável ou com timeout; as funções internas apenas observam `<-ctx.Done()` e retornam `ctx.Err()`. Contexto é imutável: `WithCancel`, `WithTimeout` e `WithValue` derivam um filho sem alterar o pai. **Falha ou limite que ele trata.** Sem ownership claro, goroutines órfãs continuam trabalhando depois que o cliente desistiu, o deploy desligou ou o timeout estourou — desperdiçando CPU, conexões e memória. O limite do mecanismo: contexto não cancela código por força; se a função ignorar `ctx.Done()` ou bloquear em operação sem suporte a contexto, o cancelamento nunca acontece. **Exemplo de aplicação.** Cenário didático: uma busca com timeout que abandona o trabalho lento. ```go package main import ( "context" "fmt" "time" ) func fetch(ctx context.Context, id int) (string, error) { timer := time.NewTimer(2 * time.Second) defer timer.Stop() select { case <-timer.C: return fmt.Sprintf("item-%d", id), nil case <-ctx.Done(): return "", ctx.Err() } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() result, err := fetch(ctx, 42) if err != nil { fmt.Println("cancelado:", err) return } fmt.Println(result) } ``` **Quando escolher outra abordagem.** Use valores de contexto apenas para dados de escopo de requisição (identificador de correlação, credenciais de chamada). Nunca use contexto para parâmetros obrigatórios da função nem para estado mutável compartilhado — passe argumentos explícitos. Se o cancelamento precisa interromper computação que não observa contexto (loop apertado de CPU), verifique `ctx.Done()` manualmente a cada iteração ou reestruture o trabalho em etapas interrompíveis. ### 3. Channels e sincronização: quando o mutex é melhor **Mecanismo.** Channels transferem *posse de dados* entre goroutines e sincronizam remetente e receptor; `sync.Mutex` (e `sync.RWMutex`) protegem *acesso a estado compartilhado*. A regra prática: use channels para orquestrar (sinalizar conclusão, distribuir tarefas, aplicar backpressure) e mutex para guardar invariantes de uma estrutura acessada por várias goroutines (contadores, caches, mapas). **Falha ou limite que ele trata.** Channels evitam condição de corrida por construção quando o dado atravessa o canal em vez de ser compartilhado. O limite: modelar todo estado compartilhado com uma goroutine "dona" e canais de pedido/resposta adiciona latência, complexidade e risco de deadlock quando um simples mutex resolveria. Inversamente, proteger um pipeline inteiro com um único mutex gigante serializa trabalho que poderia fluir em paralelo. **Exemplo de aplicação.** Cenário didático: o mesmo contador implementado das duas formas para comparar. ```go package main import ( "fmt" "sync" ) // Com mutex: direto para estado compartilhado simples. type Counter struct { mu sync.Mutex n int } func (c *Counter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.n++ } // Com channel: a goroutine dona centraliza as atualizações. func runCounterOwner(increments int) int { inc := make(chan struct{}) done := make(chan int) go func() { total := 0 for range inc { total++ } done <- total }() var wg sync.WaitGroup for i := 0; i < increments; i++ { wg.Add(1) go func() { defer wg.Done() inc <- struct{}{} }() } wg.Wait() close(inc) return <-done } func main() { var c Counter var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() c.Inc() }() } wg.Wait() fmt.Println("mutex:", c.n) fmt.Println("owner:", runCounterOwner(100)) } ``` **Quando escolher outra abordagem.** Prefira `sync.Mutex`/`sync.RWMutex` para proteger mapas, contadores e caches com acesso concorrente simples; prefira `sync.Map` apenas quando houver padrão comprovado de muitas leituras e poucas escritas com chaves disjuntas. Prefira channels quando precisar de fila, fan-out/fan-in, timeout via `select` ou backpressure natural com canal com buffer. Evite expor canais internos como API de uma estrutura com estado pequeno — o mutex mantém a interface síncrona e mais fácil de testar. ### 4. Concorrência limitada e backpressure **Mecanismo.** Concorrência limitada impõe um teto de trabalhos simultâneos com semáforo (canal com buffer de vagas), pool de workers ou `errgroup.Group` com limite. Backpressure é o efeito: quando o teto é atingido, novos trabalhos esperam em vez de consumir memória, conexões e descritores de arquivo sem controle. O tamanho do buffer do canal e o número de workers são parâmetros de capacidade, não detalhes de implementação. **Falha ou limite que ele trata.** Sem limite, um pico de entrada cria uma goroutine por item, esgota conexões com o banco, estoura memória com buffers acumulados e derruba o processo. O limite do mecanismo: teto baixo demais subutiliza recursos e aumenta latência de fila; teto alto demais apenas desloca o gargalo para o serviço seguinte. **Exemplo de aplicação.** Cenário didático: buscar URLs com no máximo 3 requisições simultâneas. ```go package main import ( "context" "fmt" "sync" "time" ) func fetchURL(ctx context.Context, url string) error { select { case <-time.After(100 * time.Millisecond): fmt.Println("ok:", url) return nil case <-ctx.Done(): return ctx.Err() } } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() urls := []string{"a", "b", "c", "d", "e", "f", "g", "h"} const maxInflight = 3 sem := make(chan struct{}, maxInflight) var wg sync.WaitGroup loop: for _, u := range urls { select { case sem <- struct{}{}: // adquire a vaga antes de iniciar a goroutine case <-ctx.Done(): break loop } wg.Add(1) go func(url string) { defer wg.Done() defer func() { <-sem }() // libera vaga _ = fetchURL(ctx, url) }(u) } wg.Wait() } ``` **Quando escolher outra abordagem.** Se a ordem de conclusão importar ou os erros precisarem encerrar o lote, use `errgroup` com limite em vez de `WaitGroup` manual. Se o produtor é muito mais rápido que o consumidor de forma sustentada, limitar concorrência não basta: adicione descarte com `select`/`default`, fila persistente ou controle de admissão na borda (limite de taxa, circuit breaker). Para I/O com latência dominada por espera, o teto pode ser maior que o número de CPUs; para CPU-bound, mantenha próximo de `GOMAXPROCS`. ### 5. Consumidores resilientes: ACK, idempotência, retry e ordem por chave **Mecanismo.** Um consumidor resiliente separa *receber*, *processar* e *confirmar* (ACK). A mensagem só recebe ACK após o efeito ser durável; o processamento é idempotente (repetir a mesma mensagem produz o mesmo estado); falhas transitórias usam retry com backoff e limite de tentativas; falhas persistentes vão para uma fila de mensagens mortas (DLQ); e a ordem só é garantida dentro de uma chave de partição, processada por um único worker por vez. **Falha ou limite que ele trata.** Sem esse desenho, três falhas clássicas aparecem: confirmar antes de processar perde mensagens em caso de queda; processar sem idempotência duplica efeitos (cobrança, envio, escrita) quando o broker reentrega; reprocessar sem DLQ trava o consumidor em mensagem envenenada. O limite: garantia global de ordem e exatamente-uma-vez de ponta a ponta não existem em sistemas distribuídos práticos — o desenho entrega *ordem por chave* e *efeito único via idempotência*. **Exemplo de aplicação.** Cenário didático e simulação sequencial e volátil: mapa em memória, DLQ em slice e ACK lógico não têm durabilidade e não demonstram particionamento — o campo `Key` é apenas um rótulo, sem worker por partição. Serve só para treinar a sequência receber → processar → confirmar. ```go package main import ( "errors" "fmt" "sync" "time" ) type Message struct { ID string // chave de idempotência (simulada) Key string // rótulo didático: não demonstra particionamento Body string } // Store é simulação sequencial e volátil: perde o estado ao reiniciar. type Store struct { mu sync.Mutex done map[string]bool } func (s *Store) AlreadyProcessed(id string) bool { s.mu.Lock() defer s.mu.Unlock() return s.done[id] } func (s *Store) MarkProcessed(id string) { s.mu.Lock() defer s.mu.Unlock() s.done[id] = true } func handle(m Message) error { if m.Body == "poison" { return errors.New("falha persistente") } fmt.Println("processada:", m.ID) return nil } func consume(messages []Message, store *Store) { var dlq []Message // simulação volátil: perde o conteúdo ao reiniciar for _, m := range messages { // processamento sequencial: sem concorrência nem partição por chave if store.AlreadyProcessed(m.ID) { continue // reentrega simulada } var err error for attempt := 1; attempt <= 3; attempt++ { if err = handle(m); err == nil { break } time.Sleep(time.Duration(attempt) * 50 * time.Millisecond) } if err != nil { dlq = append(dlq, m) // simula mover para DLQ e confirmar, sem durabilidade continue } store.MarkProcessed(m.ID) // ACK lógico simulado, sem efeito durável } fmt.Println("dlq:", len(dlq)) } func main() { store := &Store{done: map[string]bool{}} msgs := []Message{ {ID: "1", Key: "pedido-7", Body: "ok"}, {ID: "1", Key: "pedido-7", Body: "ok"}, // reentrega {ID: "2", Key: "pedido-7", Body: "poison"}, } consume(msgs, store) } ``` **Quando escolher outra abordagem.** Se o efeito for naturalmente idempotente (escrita com `SET` pela chave, upsert com versão), a chave de idempotência pode ser a própria chave de negócio. Se a ordem global for requisito real — e não apenas conveniência — reduza o paralelismo a um único consumidor ou reparticione por chave; o custo é vazão menor. Em produção, troque a simulação em memória por armazenamento durável: a verificação de idempotência e a confirmação (claim) precisam ser atômicas e duráveis, e a ordem por chave exige partição por chave com um único worker ativo por partição. ### 6. Shutdown gracioso **Mecanismo.** Shutdown gracioso converte um sinal de término (`SIGINT`/`SIGTERM`) em uma sequência ordenada: parar de aceitar trabalho novo, cancelar contextos, aguardar trabalhos em voo com timeout e só então encerrar recursos (servidor HTTP, consumidores, pools de conexão). Em Go, o padrão combina `signal.NotifyContext` (ou `os/signal`), `http.Server.Shutdown` e `sync.WaitGroup` para acompanhar goroutines de fundo. **Falha ou limite que ele trata.** Sem essa sequência, o processo morre no meio de requisições e confirmações: clientes recebem conexões cortadas, mensagens voltam para a fila sem controle e arquivos/escritas ficam pela metade. O limite: o tempo de graça é finito — trabalhos que ignoram o contexto de shutdown estouram o timeout e são interrompidos de qualquer forma, então cada etapa precisa observar o cancelamento. **Exemplo de aplicação.** Cenário didático limitado a servidor HTTP sem workers de fundo: drena apenas conexões HTTP ao receber `Ctrl+C`. Workers de fundo exigiriam `WaitGroup`/drenagem adicional, não cobertos aqui. ```go package main import ( "context" "fmt" "net/http" "os/signal" "syscall" "time" ) func main() { mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) server := &http.Server{Addr: ":8080", Handler: mux} ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() go func() { fmt.Println("ouvindo em :8080") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { fmt.Println("erro:", err) } }() <-ctx.Done() // sinal recebido: parar de aceitar, drenar o resto shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = server.Shutdown(shutdownCtx) fmt.Println("encerrado com graça") } ``` **Quando escolher outra abordagem.** O exemplo acima cobre só o HTTP; se houver workers de fundo, acompanhe-os com `sync.WaitGroup` e drenagem adicional antes de concluir o shutdown. Para CLIs e jobs de lote sem rede, basta propagar o contexto de sinal às etapas e aguardar o `WaitGroup` — sem servidor HTTP. Em orquestradores que enviam `SIGKILL` após o período de graça, dimensione o timeout de shutdown abaixo do limite da plataforma (por exemplo, `terminationGracePeriod`). Se trabalhos em voo não puderem ser interrompidos com segurança, prefira drenagem com checkpoint e retomada em vez de tentar estender o timeout indefinidamente. ### 7. Observabilidade: logs, métricas e traces que explicam o sistema **Mecanismo.** Observabilidade combina três sinais com o mesmo vocabulário de rótulos: logs estruturados para eventos discretos (com identificador de correlação), métricas para comportamento agregado (contadores, histogramas de latência, gauges de fila e de goroutines) e traces para seguir uma requisição através de goroutines e serviços. Em Go, isso significa propagar o identificador pelo `context`, expor métricas no formato do coletor usado e instrumentar fronteiras (HTTP, fila, banco) em vez de cada função interna. **Falha ou limite que ele trata.** Sem instrumentação nas fronteiras, incidentes de concorrência são invisíveis: fila crescendo, workers saturados, retries multiplicando carga e timeouts encadeados aparecem apenas como "lentidão". O limite: instrumentação excessiva (logar cada iteração, cardinalidade alta em rótulos como IDs únicos) custa CPU, memória e armazenamento — e pode derrubar o próprio sistema observado. **Exemplo de aplicação.** Cenário didático: worker que registra correlação, latência e profundidade da fila sem dependências externas. ```go package main import ( "context" "log/slog" "time" ) type ctxKey string const requestIDKey ctxKey = "request_id" func processOrder(ctx context.Context, orderID string) { start := time.Now() logger := slog.With("request_id", ctx.Value(requestIDKey), "order", orderID) logger.InfoContext(ctx, "inicio") defer func() { // Em um sistema real, observe este valor em um histograma. logger.InfoContext(ctx, "fim", "duracao_ms", time.Since(start).Milliseconds()) }() time.Sleep(50 * time.Millisecond) } func main() { ctx := context.WithValue(context.Background(), requestIDKey, "req-123") queueDepth := 7 // em um sistema real, exponha como gauge slog.InfoContext(ctx, "worker", "fila", queueDepth) processOrder(ctx, "pedido-7") } ``` **Quando escolher outra abordagem.** Para depuração local e exercícios, logs estruturados bastam; adicione métricas quando precisar de alertas e comparação entre deploys, e traces quando o caminho atravessar múltiplos serviços ou filas. Se a cardinalidade explodir, agregue por rota padrão, código de status e nome de operação — nunca por ID individual. Se o custo de coleta ficar alto, amostre traces e mantenha logs de erro completos, não o inverso. ### 8. Profiling: CPU, memória e goroutines bloqueadas **Mecanismo.** Profiling coleta amostras do que o programa realmente faz: perfil de CPU mostra onde o tempo é gasto, perfil de heap mostra onde a memória é alocada, e os perfis de goroutine e bloqueio mostram onde a concorrência trava (espera em mutex, canal vazio, I/O). O fluxo padrão é reproduzir a carga, capturar com `net/http/pprof` ou `runtime/pprof`, comparar antes/depois e só então otimizar o caminho quente comprovado. **Falha ou limite que ele trata.** Sem perfil, otimizações miram o lugar errado: reduz-se alocação em código frio enquanto o gargalo real é contenção de lock ou milhares de goroutines paradas no mesmo canal. O limite: perfis são amostras estatísticas, não verdades absolutas — cargas sintéticas curtas e benchmarks sem representatividade produzem conclusões falsas, e ativar profiling contínuo com overhead alto em produção pode distorcer as medições. **Exemplo de aplicação.** Cenário didático: expor o endpoint de profiling em um servidor de exercício e capturar CPU por 30 segundos. ```go package main import ( "fmt" "net/http" _ "net/http/pprof" ) func busy(n int) int { total := 0 for i := 0; i < n; i++ { total += i * i } return total } var sink int func main() { // Em exercício local: http://localhost:6060/debug/pprof/ go func() { if err := http.ListenAndServe("localhost:6060", nil); err != nil && err != http.ErrServerClosed { fmt.Println("pprof:", err) } }() // Carga contínua durante a captura; mantém o servidor vivo. for { sink = busy(1_000_000) } } ``` ```sh ## Captura 30s de CPU e abre o relatório interativo: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 ## Dentro do pprof: top, list busy, web ``` **Quando escolher outra abordagem.** Se o sintoma for memória crescente, comece pelo perfil de heap (`/debug/pprof/heap`) e pelo gráfico de goroutines antes do perfil de CPU — vazamento de goroutine aparece como contagem que nunca cai. Para contenção de locks, ative `runtime.SetMutexProfileFraction` e `SetBlockProfileRate` em ambiente de teste, não permanentemente em produção. Se o gargalo estiver fora do processo (banco lento, rede, broker), profiling local não ajuda: volte à observabilidade (seção 7) e meça latência por fronteira antes de micro-otimizar Go. ### Referências oficiais - Runtime do Go 1.14 (preempção assíncrona de goroutines): https://go.dev/doc/go1.14#runtime - `runtime.GOMAXPROCS`: https://pkg.go.dev/runtime#GOMAXPROCS - Pacote `context`: https://pkg.go.dev/context - Context e cancelamento: https://go.dev/blog/context - Effective Go: https://go.dev/doc/effective_go - `sync.Mutex`: https://pkg.go.dev/sync#Mutex - `sync.Map`: https://pkg.go.dev/sync#Map - `errgroup`: https://pkg.go.dev/golang.org/x/sync/errgroup - `time.NewTimer`: https://pkg.go.dev/time#NewTimer - `http.Server.Shutdown`: https://pkg.go.dev/net/http#Server-Shutdown - `signal.NotifyContext`: https://pkg.go.dev/os/signal#NotifyContext - `log/slog`: https://pkg.go.dev/log/slog - `net/http/pprof`: https://pkg.go.dev/net/http/pprof - `runtime/pprof`: https://pkg.go.dev/runtime/pprof - Diagnóstico de programas Go: https://go.dev/doc/diagnostics - Encerramento de pods (pod termination): https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination --- # TypeScript Clean Architecture: Core, Adapters, and Infra Source: articles/typescript-cleanarch-en.md > A Clean Architecture derivation for TypeScript backends: Core with usecases and protocols, bidirectional Adapters, and NestJS Infra with dependency injection. Software development changes all the time. Weak architecture becomes expensive maintenance, slow features, hard tests, and bugs that are hard to isolate. It is worth investing in a structure that supports evolution without rewriting the system at every business pressure. ### A bit of history Clean Architecture is the name Robert C. Martin (Uncle Bob) gave, in 2012, in the book *Clean Architecture: A Craftsman's Guide to Software Structure and Design*. The proposal avoids the rigidity of architectures coupled to framework and database: the core stays stable; external details change. The idea draws from DDD, SOLID, Onion Architecture, and Hexagonal Architecture. ### General proposal This article describes Clean Architecture and a practical derivation for TypeScript backends: three layers — **Core**, **Adapters**, and **Infra**. - **Core** — business rules and domain entities. Innermost layer. - **Infra** — external connections: concrete repositories, REST controllers, DI modules, framework boilerplate. - **Adapters** — mediation in both directions. A controller does not call a "raw" usecase: it goes through a service. A usecase does not talk to the database: it talks to a protocol that an adapter (repository, connector, handler) implements. Each layer has different capabilities and constraints; SOLID weighs more in Core. It works for HTTP CRUD and for systems with several frameworks and channels. Concrete benefits: clear responsibilities (reading and maintenance), flexibility to swap plugins without rewriting rules, and isolated tests per layer. ### Layer guide Example: user CRUD over REST with NestJS. Installation details are out of scope. **Core-to-infra** writing (inside out). ### Core In the classic design, *domain* and *entities* sit very close. Here they form the **Core**: everything the business rule *is* — features and domain representations. In the example, the main entity is User (`id`, `name`), in `core/entities`. #### Entities ```typescript // core/entities/UserEntity.ts export interface UserEntityProps { id?: string; name: string; } export class UserEntity { constructor(private readonly props: UserEntityProps) {} get id(): string { return this.props.id ?? ""; } get name(): string { return this.props.name; } } ``` The entity receives typed props and exposes getters. It depends on an interface any transfer DTO can satisfy later. #### Features and usecases CRUD needs create, fetch, update, and remove. In Core, each usecase implements a contract (feature) with a single public method — aligned with Liskov, open/closed, interface segregation, and single responsibility. The usecase does **not** access the database: it knows **protocols** describing the external action (dependency inversion). Registration: name is required; if it already exists, error; otherwise return `UserEntity`. - contract `CreateUser` - implementation `CreateUserUsecase` In TypeScript, an abstract class with abstract methods works as both contract *and* value — useful for DI (`const createUserSymbol = CreateUser`): ```typescript // core/features/CreateUser.ts export abstract class CreateUser { abstract execute(name: string): Promise; } ``` ```typescript // core/usecases/CreateUserUsecase.ts export class CreateUserUsecase implements CreateUser { constructor( private readonly createUserProtocol: CreateUserProtocol, private readonly getByNameProtocol: GetUserByNameProtocol, ) {} async execute(name: string): Promise { const existsName = await this.getByNameProtocol.getByName(name); if (existsName) { throw new UserAlreadyExistsException( `the name ${name} already exists`, ); } return this.createUserProtocol.register(name); } } ``` The usecase defines *what* (validate name, register). It does not define *how* to fetch or persist. The rule stays independent of lib, framework, and database. Watch out: a usecase that only delegates to the protocol without validating may be pushing business rules into the adapter. In `CreateUserUsecase`, the duplicate-name check is Core's obligation. #### Exceptions `UserAlreadyExistsException` belongs to Core: an invalid rule flow is also a rule. Each failure mapped to a known exception helps maintenance. Base with `code` (later becomes HTTP status at the edge): ```typescript // core/exceptions/IBaseException.ts export abstract class IBaseException extends Error { code: number; constructor(message: string) { super(message); } } ``` ```typescript // core/exceptions/UserAlreadyExistsException.ts export class UserAlreadyExistsException extends IBaseException { constructor(message?: string) { super(message ?? "User already exists"); this.code = 400; } } ``` The usecase **throws** exceptions; it does **not** handle them. Mapping unknown type → known type belongs in adapter or infra. #### Protocols `CreateUserProtocol` and `GetUserByNameProtocol` are contracts for external-device access. A protocol exists to inform or trigger external action — **not** to process business rules. Preference: one public method per protocol. ```typescript // core/protocols/CreateUserProtocol.ts export abstract class CreateUserProtocol { abstract register(name: string): Promise; } ``` ```typescript // core/protocols/GetUserByNameProtocol.ts export abstract class GetUserByNameProtocol { abstract getByName(name: string): Promise; } ``` Core is the center; the adaptation layer connects the rest. ### Adapter Adapters control bidirectional traffic: external → rule and rule → external. They adapt objects, parameters, and exceptions — the same spirit as the [Adapter pattern](/en/articles/design-patterns-adapter/). Two groups: 1. Called by Core — implement at least one protocol. 2. Called by Infra — generally **services**. #### Connectors, handlers, and repositories Classes implementing protocols. Each adapts **one** external device (ORM, HTTP client, queue, filesystem). Naming convention: - **Repositories** — protocol tied to a database (familiar vocabulary). - **Connectors** — return data without being a "table" (e.g. `ClientHttpFetchConnector`, `ClientHttpAxiosConnector`). - **Handlers** — process without synchronous return (e.g. publishing to Kafka). Other names are valid; the criterion is one adapter per device. In the CRUD, only repository (mock): ```typescript // adapters/repositories/UsersMockRepository.ts export class UsersMockRepository implements GetUserByIdProtocol, GetUserByNameProtocol, CreateUserProtocol, UpdateUserProtocol, DeleteUserProtocol { private db: DbConnector; constructor() { this.db = mockDbConnector; } async getById(id: string): Promise { return this.db.users.getById(id); } async getByName(name: string): Promise { return this.db.users.getByName(name); } async register(name: string): Promise { return this.db.users.register(name); } async update(id: string, name: string): Promise { return this.db.users.update(id, name); } async delete(id: string): Promise { return this.db.users.delete(id); } } ``` Mock connector: ```typescript export const mockDbConnector: DbConnector = { users: { getById: async (id: string) => Promise.resolve(new UserEntity({ id, name: "Test" })), getByName: async (name: string) => Promise.resolve(new UserEntity({ id: "1", name })), register: async (name: string) => Promise.resolve(new UserEntity({ id: "2", name })), update: async (id: string, name: string) => Promise.resolve(new UserEntity({ id, name })), delete: async (_id: string) => Promise.resolve(), }, profiles: { getById: async (_id: string) => Promise.resolve(null), getByName: async (_name: string) => Promise.resolve(null), register: async (_name: string) => Promise.resolve(null), update: async (_id: string, _name: string) => Promise.resolve(null), delete: async (_id: string) => Promise.resolve(), }, }; ``` `UserEntity` (Core) is not the database table. They are different things. **"Does implementing several protocols violate the S in SOLID?"** It depends. Splitting each protocol into its own class maximizes SRP. Keeping one repository per entity/aggregate is also coherent: the scope is "User data in this connector". Practical criteria for *not* joining A and B in the same class: 1. Implementing B would require another external dependency. 2. A and B work on different entities/scopes. 3. Cyclomatic (or cognitive) complexity rises too much. 4. The class passes ~500 lines — subjective threshold; use team judgment. With DI and interface segregation, `CreateUserUsecase` only knows `CreateUserProtocol` and `GetUserByNameProtocol`. At runtime it may receive the same `UsersMockRepository` instance in both parameters — without coupling to the concrete class. #### Services Services adapt Infra calls into Core: REST → service → usecase → protocol → repository → database. The same design holds for a queue handler or GraphQL resolver: different input, service in the middle. ```typescript export class UserService { constructor( private createUserUsecase: CreateUser, private updateUserUsecase: UpdateUser, private deleteUserUsecase: DeleteUser, private getUserUsecase: GetUser, ) {} async getUser(id: string): Promise { try { return await this.getUserUsecase.execute(id); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async createUser(name: string): Promise { try { return await this.createUserUsecase.execute(name); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async updateUser(id: string, name: string): Promise { try { return await this.updateUserUsecase.execute(id, name); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async deleteUser(id: string): Promise { try { return await this.deleteUserUsecase.execute(id); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } } ``` The service depends on **features** (contracts), not concrete usecase classes. It maps Core exceptions to `RestError`, which Infra translates into HTTP responses. Good practice: services only for Infra; one service per input "instrument" (HTTP controller ≠ queue handler), except middleware in the same stack reusing the same service. ### Infra Framework, DI, controllers, DTOs, and boilerplate that is neither business nor adapter. NestJS controller: ```typescript // infra/controllers/UserController.ts @Controller("users") export class UserController { constructor(private service: UserService) {} @Get(":id") async getUser(@Param("id") id: string): Promise { return this.service.getUser(id); } @Post() async createUser(@Body() user: UserDto): Promise { return this.service.createUser(user.name); } @Put(":id") async updateUser( @Param("id") id: string, @Body() user: UserDto, ): Promise { return this.service.updateUser(id, user.name); } @Delete(":id") async deleteUser(@Param("id") id: string): Promise { return this.service.deleteUser(id); } } ``` DTO: ```typescript // infra/dtos/UserDto.ts export class UserDto implements UserEntityProps { id?: string; name: string; constructor(props: UserEntityProps) { this.id = props.id; this.name = props.name; } } ``` DI module: ```typescript // infra/modules/UserModule.ts @Module({ imports: [], controllers: [UserController], providers: [ UserService, { provide: CreateUser, useClass: CreateUserUsecase }, { provide: UpdateUser, useClass: UpdateUserUsecase }, { provide: DeleteUser, useClass: DeleteUserUsecase }, { provide: GetUser, useClass: GetUserUsecase }, { provide: CreateUserProtocol, useClass: UserPrismaRepository }, { provide: UpdateUserProtocol, useClass: UserPrismaRepository }, { provide: DeleteUserProtocol, useClass: UserPrismaRepository }, { provide: GetUserByNameProtocol, useClass: UserPrismaRepository }, { provide: GetUserByIdProtocol, useClass: UserPrismaRepository }, ], }) export class UserModule {} ``` Infra is freer: it depends on tooling and exists to sustain Core. ### Execution flow Controller (Infra) → Service (Adapter) → Usecase (Core) → Protocol (contract) → Repository (Adapter) → database (Infra). The usecase does not import the concrete repository; the service does not import the concrete usecase class. The dependency arrow points inward. ### Complementary CRUD Beyond registration: #### Fetch ```typescript // core/features/GetUser.ts export abstract class GetUser { abstract execute(id: string): Promise; } // core/usecases/GetUserUsecase.ts export class GetUserUsecase implements GetUser { constructor(private readonly getProtocol: GetUserByIdProtocol) {} async execute(id: string): Promise { return this.getProtocol.getById(id); } } ``` #### Update ```typescript // core/features/UpdateUser.ts export abstract class UpdateUser { abstract execute(id: string, name: string): Promise; } // core/usecases/UpdateUserUsecase.ts export class UpdateUserUsecase implements UpdateUser { constructor( private readonly updateProtocol: UpdateUserProtocol, private readonly getByIdProtocol: GetUserByIdProtocol, ) {} async execute(id: string, name: string): Promise { const exists = await this.getByIdProtocol.getById(id); if (!exists) { throw new UserNotExistsException(`User with id ${id} not exists`); } return this.updateProtocol.update(id, name); } } ``` #### Delete ```typescript // core/features/DeleteUser.ts export abstract class DeleteUser { abstract execute(id: string): Promise; } // core/usecases/DeleteUserUsecase.ts export class DeleteUserUsecase implements DeleteUser { constructor( private readonly deleteProtocol: DeleteUserProtocol, private readonly getByIdProtocol: GetUserByIdProtocol, ) {} async execute(id: string): Promise { const exists = await this.getByIdProtocol.getById(id); if (!exists) { throw new UserNotExistsException(`User with id ${id} not exists`); } return this.deleteProtocol.delete(id); } } ``` #### Remaining exception and protocols ```typescript export class UserNotExistsException extends IBaseException { constructor(message: string) { super(message); this.code = 404; } } ``` ```typescript // core/protocols/GetUserByIdProtocol.ts export abstract class GetUserByIdProtocol { abstract getById(id: string): Promise; } // core/protocols/UpdateUserProtocol.ts export abstract class UpdateUserProtocol { abstract update(id: string, name: string): Promise; } // core/protocols/DeleteUserProtocol.ts export abstract class DeleteUserProtocol { abstract delete(id: string): Promise; } ``` ### Conclusion Separating Core, Adapters, and Infra leaves business rules testable without Nest, Prisma, or HTTP. Swapping databases or input channels becomes an adapter swap plus DI wiring — not a usecase rewrite. The cost is more files and boundary discipline; the gain shows when the system must change without dragging the domain along. Originally published on [Medium](https://medium.com/@contato.dev.rafael.pereira/typescript-cleanarch-668935d677c2) (03/15/2023). --- # TypeScript Clean Architecture: Core, Adapters e Infra Source: articles/typescript-cleanarch-es.md > Derivación de Clean Architecture para backend TypeScript: Core con usecases y protocols, Adapters bidireccionales e Infra NestJS con inyección de dependencias. El desarrollo de software cambia todo el tiempo. La arquitectura débil se vuelve mantenimiento caro, feature lenta, prueba difícil y bug difícil de aislar. Vale invertir en una estructura que soporte evolución sin reescribir el sistema ante cada presión del negocio. ### Un poco de historia Clean Architecture es el nombre que Robert C. Martin (Uncle Bob) dio, en 2012, en el libro *Clean Architecture: A Craftsman's Guide to Software Structure and Design*. La propuesta huye de la rigidez de arquitecturas acopladas a framework y base: el núcleo queda estable; los detalles externos cambian. La idea bebe de DDD, SOLID, Onion Architecture y Hexagonal Architecture. ### Propuesta general Este artículo describe Clean Architecture y una derivación práctica para backends en TypeScript: tres capas — **Core**, **Adapters** e **Infra**. - **Core** — regla de negocio y entidades del dominio. Capa más interna. - **Infra** — conexiones externas: repositorios concretos, controllers REST, módulos de DI, boilerplate de framework. - **Adapters** — intermediación en ambos sentidos. El controller no llama al usecase "crudo": pasa por un servicio. El usecase no habla con la base: habla con un protocolo que un adapter (repositorio, connector, handler) implementa. Cada capa tiene capacidades y restricciones distintas; SOLID pesa más en el Core. Sirve para CRUD HTTP y para sistemas con varios frameworks y canales. Beneficios concretos: responsabilidades claras (lectura y mantenimiento), flexibilidad para cambiar plugin sin reescribir regla, y pruebas aisladas por capa. ### Guía de capas Ejemplo: CRUD de usuarios vía REST con NestJS. Detalles de instalación quedan fuera. Escritura **core-to-infra** (de dentro hacia fuera). ### Core En el diseño clásico, *domain* y *entities* quedan muy próximas. Aquí forman el **Core**: todo lo que la regla de negocio *es* — funcionalidades y representaciones del dominio. En el ejemplo, la entidad principal es Usuario (`id`, `name`), en `core/entities`. #### Entities ```typescript // core/entities/UserEntity.ts export interface UserEntityProps { id?: string; name: string; } export class UserEntity { constructor(private readonly props: UserEntityProps) {} get id(): string { return this.props.id ?? ""; } get name(): string { return this.props.name; } } ``` La entidad recibe props tipadas y expone getters. Depende de una interfaz que cualquier DTO de transferencia puede satisfacer después. #### Features y usecases El CRUD necesita crear, buscar, actualizar y eliminar. En el Core, cada usecase implementa un contrato (feature) con un único método público — alineado a Liskov, abierto/cerrado, segregación de interfaz y responsabilidad única. El usecase **no** accede a la base: conoce **protocols** que describen la acción externa (inversión de dependencia). Registro: nombre obligatorio; si ya existe, error; si no, retorna `UserEntity`. - contrato `CreateUser` - implementación `CreateUserUsecase` En TypeScript, clase abstracta con métodos abstractos funciona como contrato *y* valor — útil para DI (`const createUserSymbol = CreateUser`): ```typescript // core/features/CreateUser.ts export abstract class CreateUser { abstract execute(name: string): Promise; } ``` ```typescript // core/usecases/CreateUserUsecase.ts export class CreateUserUsecase implements CreateUser { constructor( private readonly createUserProtocol: CreateUserProtocol, private readonly getByNameProtocol: GetUserByNameProtocol, ) {} async execute(name: string): Promise { const existsName = await this.getByNameProtocol.getByName(name); if (existsName) { throw new UserAlreadyExistsException( `the name ${name} already exists`, ); } return this.createUserProtocol.register(name); } } ``` El usecase define *qué* (validar nombre, registrar). No define *cómo* buscar o persistir. La regla queda independiente de lib, framework y base. Cuidado: un usecase que solo delega al protocol sin validar puede estar empujando regla de negocio hacia el adapter. En `CreateUserUsecase`, la verificación de nombre duplicado es obligación del Core. #### Exceptions `UserAlreadyExistsException` pertenece al Core: el flujo inválido de la regla también es regla. Cada fallo mapeado a una excepción conocida ayuda al mantenimiento. Base con `code` (después se vuelve status HTTP en el borde): ```typescript // core/exceptions/IBaseException.ts export abstract class IBaseException extends Error { code: number; constructor(message: string) { super(message); } } ``` ```typescript // core/exceptions/UserAlreadyExistsException.ts export class UserAlreadyExistsException extends IBaseException { constructor(message?: string) { super(message ?? "User already exists"); this.code = 400; } } ``` El usecase **lanza** excepciones; **no** las trata. Mapear tipo desconocido → tipo conocido queda en adapter o infra. #### Protocols `CreateUserProtocol` y `GetUserByNameProtocol` son contratos de acceso a dispositivo externo. El protocol existe para informar o disparar acción externa — **no** para procesar regla de negocio. Preferencia: un método público por protocol. ```typescript // core/protocols/CreateUserProtocol.ts export abstract class CreateUserProtocol { abstract register(name: string): Promise; } ``` ```typescript // core/protocols/GetUserByNameProtocol.ts export abstract class GetUserByNameProtocol { abstract getByName(name: string): Promise; } ``` El Core es el centro; la capa de adaptación conecta el resto. ### Adapter Los adapters controlan el tráfico bidireccional: externo → regla y regla → externo. Adaptan objetos, parámetros y excepciones — el mismo espíritu del [patrón Adapter](/es/articulos/design-patterns-adapter/). Dos grupos: 1. Llamados por el Core — implementan al menos un protocol. 2. Llamados por la Infra — en general **services**. #### Connectors, handlers y repositories Clases que implementan protocols. Cada una adapta **un** dispositivo externo (ORM, cliente HTTP, cola, filesystem). Convención de nombres: - **Repositories** — protocol ligado a base (vocabulario familiar). - **Connectors** — retornan datos sin ser "tabla" (ej.: `ClientHttpFetchConnector`, `ClientHttpAxiosConnector`). - **Handlers** — procesan sin retorno síncrono (ej.: publicar en Kafka). Otros nombres son válidos; el criterio es un adapter por dispositivo. En el CRUD, solo repository (mock): ```typescript // adapters/repositories/UsersMockRepository.ts export class UsersMockRepository implements GetUserByIdProtocol, GetUserByNameProtocol, CreateUserProtocol, UpdateUserProtocol, DeleteUserProtocol { private db: DbConnector; constructor() { this.db = mockDbConnector; } async getById(id: string): Promise { return this.db.users.getById(id); } async getByName(name: string): Promise { return this.db.users.getByName(name); } async register(name: string): Promise { return this.db.users.register(name); } async update(id: string, name: string): Promise { return this.db.users.update(id, name); } async delete(id: string): Promise { return this.db.users.delete(id); } } ``` Mock del conector: ```typescript export const mockDbConnector: DbConnector = { users: { getById: async (id: string) => Promise.resolve(new UserEntity({ id, name: "Test" })), getByName: async (name: string) => Promise.resolve(new UserEntity({ id: "1", name })), register: async (name: string) => Promise.resolve(new UserEntity({ id: "2", name })), update: async (id: string, name: string) => Promise.resolve(new UserEntity({ id, name })), delete: async (_id: string) => Promise.resolve(), }, profiles: { getById: async (_id: string) => Promise.resolve(null), getByName: async (_name: string) => Promise.resolve(null), register: async (_name: string) => Promise.resolve(null), update: async (_id: string, _name: string) => Promise.resolve(null), delete: async (_id: string) => Promise.resolve(), }, }; ``` `UserEntity` (Core) no es la tabla de la base. Son cosas distintas. **"¿Implementar varios protocols hiere la S de SOLID?"** Depende. Separar cada protocol en clase propia maximiza SRP. Mantener un repository por entidad/agregado también es coherente: el alcance es "datos de User en este conector". Criterios prácticos para *no* juntar A y B en la misma clase: 1. Implementar B exigiría otra dependencia externa. 2. A y B trabajan entidades/alcances distintos. 3. La complejidad ciclomática (o cognitiva) sube demasiado. 4. La clase pasa de ~500 líneas — umbral subjetivo; usa criterio de equipo. Con DI y segregación de interfaz, `CreateUserUsecase` solo conoce `CreateUserProtocol` y `GetUserByNameProtocol`. En runtime puede recibir la misma instancia de `UsersMockRepository` en los dos parámetros — sin acoplarse a la clase concreta. #### Services Los services adaptan llamadas de la Infra hacia el Core: REST → service → usecase → protocol → repository → base. El mismo diseño vale para handler de cola o resolver GraphQL: entrada distinta, service en el medio. ```typescript export class UserService { constructor( private createUserUsecase: CreateUser, private updateUserUsecase: UpdateUser, private deleteUserUsecase: DeleteUser, private getUserUsecase: GetUser, ) {} async getUser(id: string): Promise { try { return await this.getUserUsecase.execute(id); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async createUser(name: string): Promise { try { return await this.createUserUsecase.execute(name); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async updateUser(id: string, name: string): Promise { try { return await this.updateUserUsecase.execute(id, name); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async deleteUser(id: string): Promise { try { return await this.deleteUserUsecase.execute(id); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } } ``` El service depende de las **features** (contratos), no de las clases concretas de usecase. Mapea excepción de Core a `RestError` que la Infra traduce en respuesta HTTP. Buena práctica: services solo para Infra; un service por "instrumento" de entrada (controller HTTP ≠ handler de cola), salvo middleware del mismo stack que reutiliza el mismo service. ### Infra Framework, DI, controllers, DTOs y boilerplate que no es negocio ni adapter. Controller NestJS: ```typescript // infra/controllers/UserController.ts @Controller("users") export class UserController { constructor(private service: UserService) {} @Get(":id") async getUser(@Param("id") id: string): Promise { return this.service.getUser(id); } @Post() async createUser(@Body() user: UserDto): Promise { return this.service.createUser(user.name); } @Put(":id") async updateUser( @Param("id") id: string, @Body() user: UserDto, ): Promise { return this.service.updateUser(id, user.name); } @Delete(":id") async deleteUser(@Param("id") id: string): Promise { return this.service.deleteUser(id); } } ``` DTO: ```typescript // infra/dtos/UserDto.ts export class UserDto implements UserEntityProps { id?: string; name: string; constructor(props: UserEntityProps) { this.id = props.id; this.name = props.name; } } ``` Módulo de DI: ```typescript // infra/modules/UserModule.ts @Module({ imports: [], controllers: [UserController], providers: [ UserService, { provide: CreateUser, useClass: CreateUserUsecase }, { provide: UpdateUser, useClass: UpdateUserUsecase }, { provide: DeleteUser, useClass: DeleteUserUsecase }, { provide: GetUser, useClass: GetUserUsecase }, { provide: CreateUserProtocol, useClass: UserPrismaRepository }, { provide: UpdateUserProtocol, useClass: UserPrismaRepository }, { provide: DeleteUserProtocol, useClass: UserPrismaRepository }, { provide: GetUserByNameProtocol, useClass: UserPrismaRepository }, { provide: GetUserByIdProtocol, useClass: UserPrismaRepository }, ], }) export class UserModule {} ``` Infra es más "libre": depende de las herramientas y existe para sostener el Core. ### Flujo de ejecución Controller (Infra) → Service (Adapter) → Usecase (Core) → Protocol (contrato) → Repository (Adapter) → base (Infra). El usecase no importa el repository concreto; el service no importa la clase concreta del usecase. La flecha de dependencia apunta hacia dentro. ### CRUD complementario Además del registro: #### Búsqueda ```typescript // core/features/GetUser.ts export abstract class GetUser { abstract execute(id: string): Promise; } // core/usecases/GetUserUsecase.ts export class GetUserUsecase implements GetUser { constructor(private readonly getProtocol: GetUserByIdProtocol) {} async execute(id: string): Promise { return this.getProtocol.getById(id); } } ``` #### Actualización ```typescript // core/features/UpdateUser.ts export abstract class UpdateUser { abstract execute(id: string, name: string): Promise; } // core/usecases/UpdateUserUsecase.ts export class UpdateUserUsecase implements UpdateUser { constructor( private readonly updateProtocol: UpdateUserProtocol, private readonly getByIdProtocol: GetUserByIdProtocol, ) {} async execute(id: string, name: string): Promise { const exists = await this.getByIdProtocol.getById(id); if (!exists) { throw new UserNotExistsException(`User with id ${id} not exists`); } return this.updateProtocol.update(id, name); } } ``` #### Eliminación ```typescript // core/features/DeleteUser.ts export abstract class DeleteUser { abstract execute(id: string): Promise; } // core/usecases/DeleteUserUsecase.ts export class DeleteUserUsecase implements DeleteUser { constructor( private readonly deleteProtocol: DeleteUserProtocol, private readonly getByIdProtocol: GetUserByIdProtocol, ) {} async execute(id: string): Promise { const exists = await this.getByIdProtocol.getById(id); if (!exists) { throw new UserNotExistsException(`User with id ${id} not exists`); } return this.deleteProtocol.delete(id); } } ``` #### Exception y protocols restantes ```typescript export class UserNotExistsException extends IBaseException { constructor(message: string) { super(message); this.code = 404; } } ``` ```typescript // core/protocols/GetUserByIdProtocol.ts export abstract class GetUserByIdProtocol { abstract getById(id: string): Promise; } // core/protocols/UpdateUserProtocol.ts export abstract class UpdateUserProtocol { abstract update(id: string, name: string): Promise; } // core/protocols/DeleteUserProtocol.ts export abstract class DeleteUserProtocol { abstract delete(id: string): Promise; } ``` ### Conclusión Separar Core, Adapters e Infra deja la regla de negocio testeable sin Nest, Prisma o HTTP. Cambiar base o canal de entrada se vuelve cambio de adapter y wiring de DI — no reescritura del usecase. El costo es más archivos y disciplina de frontera; la ganancia aparece cuando el sistema necesita cambiar sin arrastrar el dominio. Publicado originalmente en [Medium](https://medium.com/@contato.dev.rafael.pereira/typescript-cleanarch-668935d677c2) (15/03/2023). --- # TypeScript Clean Architecture: Core, Adapters e Infra Source: articles/typescript-cleanarch-pt-br.md > Derivação da Clean Architecture para backend TypeScript: Core com usecases e protocols, Adapters bidirecionais e Infra NestJS com injeção de dependências. Desenvolvimento de software muda o tempo todo. Arquitetura fraca vira manutenção cara, feature lenta, teste difícil e bug difícil de isolar. Vale investir em uma estrutura que suporte evolução sem reescrever o sistema a cada pressão do negócio. ### Um pouco de história Clean Architecture é o nome que Robert C. Martin (Uncle Bob) deu, em 2012, no livro *Clean Architecture: A Craftsman's Guide to Software Structure and Design*. A proposta foge da rigidez de arquiteturas acopladas a framework e banco: o núcleo fica estável; detalhes externos mudam. A ideia bebe de DDD, SOLID, Onion Architecture e Hexagonal Architecture. ### Proposta geral Este artigo descreve a Clean Architecture e uma derivação prática para backends em TypeScript: três camadas — **Core**, **Adapters** e **Infra**. - **Core** — regra de negócio e entidades do domínio. Camada mais interna. - **Infra** — conexões externas: repositórios concretos, controllers REST, módulos de DI, boilerplate de framework. - **Adapters** — intermediação nos dois sentidos. Controller não chama usecase “cru”: passa por um serviço. Usecase não fala com o banco: fala com um protocolo que um adapter (repositório, connector, handler) implementa. Cada camada tem capacidades e restrições diferentes; SOLID pesa mais no Core. Serve para CRUD HTTP e para sistemas com vários frameworks e canais. Benefícios concretos: responsabilidades claras (leitura e manutenção), flexibilidade para trocar plugin sem reescrever regra, e testes isolados por camada. ### Guia de camadas Exemplo: CRUD de usuários via REST com NestJS. Detalhes de instalação ficam de fora. Escrita **core-to-infra** (de dentro para fora). ### Core No desenho clássico, *domain* e *entities* ficam muito próximas. Aqui elas formam o **Core**: tudo o que a regra de negócio *é* — funcionalidades e representações do domínio. No exemplo, a entidade principal é Usuário (`id`, `name`), em `core/entities`. #### Entities ```typescript // core/entities/UserEntity.ts export interface UserEntityProps { id?: string; name: string; } export class UserEntity { constructor(private readonly props: UserEntityProps) {} get id(): string { return this.props.id ?? ""; } get name(): string { return this.props.name; } } ``` A entidade recebe props tipadas e expõe getters. Depende de uma interface que qualquer DTO de transferência pode satisfazer depois. #### Features e usecases O CRUD precisa criar, buscar, atualizar e remover. No Core, cada usecase implementa um contrato (feature) com um único método público — alinhado a Liskov, aberto/fechado, segregação de interface e responsabilidade única. O usecase **não** acessa o banco: conhece **protocols** que descrevem a ação externa (inversão de dependência). Cadastro: nome obrigatório; se já existir, erro; se não, retorna `UserEntity`. - contrato `CreateUser` - implementação `CreateUserUsecase` No TypeScript, classe abstrata com métodos abstratos funciona como contrato *e* valor — útil para DI (`const createUserSymbol = CreateUser`): ```typescript // core/features/CreateUser.ts export abstract class CreateUser { abstract execute(name: string): Promise; } ``` ```typescript // core/usecases/CreateUserUsecase.ts export class CreateUserUsecase implements CreateUser { constructor( private readonly createUserProtocol: CreateUserProtocol, private readonly getByNameProtocol: GetUserByNameProtocol, ) {} async execute(name: string): Promise { const existsName = await this.getByNameProtocol.getByName(name); if (existsName) { throw new UserAlreadyExistsException( `the name ${name} already exists`, ); } return this.createUserProtocol.register(name); } } ``` O usecase define *o quê* (validar nome, registrar). Não define *como* buscar ou persistir. A regra fica independente de lib, framework e banco. Cuidado: usecase que só delega ao protocol sem validar pode estar empurrando regra de negócio para o adapter. Em `CreateUserUsecase`, a checagem de nome duplicado é obrigação do Core. #### Exceptions `UserAlreadyExistsException` pertence ao Core: fluxo inválido da regra também é regra. Cada falha mapeada a uma exceção conhecida ajuda manutenção. Base com `code` (depois vira status HTTP na borda): ```typescript // core/exceptions/IBaseException.ts export abstract class IBaseException extends Error { code: number; constructor(message: string) { super(message); } } ``` ```typescript // core/exceptions/UserAlreadyExistsException.ts export class UserAlreadyExistsException extends IBaseException { constructor(message?: string) { super(message ?? "User already exists"); this.code = 400; } } ``` O usecase **lança** exceções; **não** as trata. Mapear tipo desconhecido → tipo conhecido fica em adapter ou infra. #### Protocols `CreateUserProtocol` e `GetUserByNameProtocol` são contratos de acesso a dispositivo externo. Protocol existe para informar ou disparar ação externa — **não** para processar regra de negócio. Preferência: um método público por protocol. ```typescript // core/protocols/CreateUserProtocol.ts export abstract class CreateUserProtocol { abstract register(name: string): Promise; } ``` ```typescript // core/protocols/GetUserByNameProtocol.ts export abstract class GetUserByNameProtocol { abstract getByName(name: string): Promise; } ``` O Core é o centro; a camada de adaptação liga o resto. ### Adapter Adapters controlam o tráfego bidirecional: externo → regra e regra → externo. Adaptam objetos, parâmetros e exceções — o mesmo espírito do [padrão Adapter](/artigos/design-patterns-adapter/). Dois grupos: 1. Chamados pelo Core — implementam pelo menos um protocol. 2. Chamados pela Infra — em geral **services**. #### Connectors, handlers e repositories Classes que implementam protocols. Cada uma adapta **um** dispositivo externo (ORM, cliente HTTP, fila, filesystem). Convenção de nomes: - **Repositories** — protocol ligado a banco (vocabulário familiar). - **Connectors** — retornam dados sem ser “tabela” (ex.: `ClientHttpFetchConnector`, `ClientHttpAxiosConnector`). - **Handlers** — processam sem retorno síncrono (ex.: publicar em Kafka). Outros nomes são válidos; o critério é um adapter por dispositivo. No CRUD, só repository (mock): ```typescript // adapters/repositories/UsersMockRepository.ts export class UsersMockRepository implements GetUserByIdProtocol, GetUserByNameProtocol, CreateUserProtocol, UpdateUserProtocol, DeleteUserProtocol { private db: DbConnector; constructor() { this.db = mockDbConnector; } async getById(id: string): Promise { return this.db.users.getById(id); } async getByName(name: string): Promise { return this.db.users.getByName(name); } async register(name: string): Promise { return this.db.users.register(name); } async update(id: string, name: string): Promise { return this.db.users.update(id, name); } async delete(id: string): Promise { return this.db.users.delete(id); } } ``` Mock do conector: ```typescript export const mockDbConnector: DbConnector = { users: { getById: async (id: string) => Promise.resolve(new UserEntity({ id, name: "Test" })), getByName: async (name: string) => Promise.resolve(new UserEntity({ id: "1", name })), register: async (name: string) => Promise.resolve(new UserEntity({ id: "2", name })), update: async (id: string, name: string) => Promise.resolve(new UserEntity({ id, name })), delete: async (_id: string) => Promise.resolve(), }, profiles: { getById: async (_id: string) => Promise.resolve(null), getByName: async (_name: string) => Promise.resolve(null), register: async (_name: string) => Promise.resolve(null), update: async (_id: string, _name: string) => Promise.resolve(null), delete: async (_id: string) => Promise.resolve(), }, }; ``` `UserEntity` (Core) não é a tabela do banco. São coisas diferentes. **“Implementar vários protocols fere o S do SOLID?”** Depende. Separar cada protocol em classe própria maximiza SRP. Manter um repository por entidade/agregado também é coerente: o escopo é “dados de User neste conector”. Critérios práticos para *não* juntar A e B na mesma classe: 1. Implementar B exigiria outra dependência externa. 2. A e B trabalham entidades/escopos diferentes. 3. Complexidade ciclomática (ou cognitiva) sobe demais. 4. A classe passa de ~500 linhas — limiar subjetivo; use senso de time. Com DI e segregação de interface, `CreateUserUsecase` só conhece `CreateUserProtocol` e `GetUserByNameProtocol`. Em runtime pode receber a mesma instância de `UsersMockRepository` nos dois parâmetros — sem acoplar à classe concreta. #### Services Services adaptam chamadas da Infra para o Core: REST → service → usecase → protocol → repository → banco. O mesmo desenho vale para handler de fila ou resolver GraphQL: entrada diferente, service no meio. ```typescript export class UserService { constructor( private createUserUsecase: CreateUser, private updateUserUsecase: UpdateUser, private deleteUserUsecase: DeleteUser, private getUserUsecase: GetUser, ) {} async getUser(id: string): Promise { try { return await this.getUserUsecase.execute(id); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async createUser(name: string): Promise { try { return await this.createUserUsecase.execute(name); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async updateUser(id: string, name: string): Promise { try { return await this.updateUserUsecase.execute(id, name); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } async deleteUser(id: string): Promise { try { return await this.deleteUserUsecase.execute(id); } catch (error) { console.error(error); throw RestError.fromBaseException(error); } } } ``` O service depende das **features** (contratos), não das classes concretas de usecase. Mapeia exceção de Core para `RestError` que a Infra traduz em resposta HTTP. Boa prática: services só para Infra; um service por “instrumento” de entrada (controller HTTP ≠ handler de fila), salvo middleware do mesmo stack que reutiliza o mesmo service. ### Infra Framework, DI, controllers, DTOs e boilerplate que não é negócio nem adapter. Controller NestJS: ```typescript // infra/controllers/UserController.ts @Controller("users") export class UserController { constructor(private service: UserService) {} @Get(":id") async getUser(@Param("id") id: string): Promise { return this.service.getUser(id); } @Post() async createUser(@Body() user: UserDto): Promise { return this.service.createUser(user.name); } @Put(":id") async updateUser( @Param("id") id: string, @Body() user: UserDto, ): Promise { return this.service.updateUser(id, user.name); } @Delete(":id") async deleteUser(@Param("id") id: string): Promise { return this.service.deleteUser(id); } } ``` DTO: ```typescript // infra/dtos/UserDto.ts export class UserDto implements UserEntityProps { id?: string; name: string; constructor(props: UserEntityProps) { this.id = props.id; this.name = props.name; } } ``` Módulo de DI: ```typescript // infra/modules/UserModule.ts @Module({ imports: [], controllers: [UserController], providers: [ UserService, { provide: CreateUser, useClass: CreateUserUsecase }, { provide: UpdateUser, useClass: UpdateUserUsecase }, { provide: DeleteUser, useClass: DeleteUserUsecase }, { provide: GetUser, useClass: GetUserUsecase }, { provide: CreateUserProtocol, useClass: UserPrismaRepository }, { provide: UpdateUserProtocol, useClass: UserPrismaRepository }, { provide: DeleteUserProtocol, useClass: UserPrismaRepository }, { provide: GetUserByNameProtocol, useClass: UserPrismaRepository }, { provide: GetUserByIdProtocol, useClass: UserPrismaRepository }, ], }) export class UserModule {} ``` Infra é mais “livre”: depende do ferramental e existe para sustentar o Core. ### Fluxo de execução Controller (Infra) → Service (Adapter) → Usecase (Core) → Protocol (contrato) → Repository (Adapter) → banco (Infra). O usecase não importa o repository concreto; o service não importa a classe concreta do usecase. A seta de dependência aponta para dentro. ### CRUD complementar Além do cadastro: #### Busca ```typescript // core/features/GetUser.ts export abstract class GetUser { abstract execute(id: string): Promise; } // core/usecases/GetUserUsecase.ts export class GetUserUsecase implements GetUser { constructor(private readonly getProtocol: GetUserByIdProtocol) {} async execute(id: string): Promise { return this.getProtocol.getById(id); } } ``` #### Atualização ```typescript // core/features/UpdateUser.ts export abstract class UpdateUser { abstract execute(id: string, name: string): Promise; } // core/usecases/UpdateUserUsecase.ts export class UpdateUserUsecase implements UpdateUser { constructor( private readonly updateProtocol: UpdateUserProtocol, private readonly getByIdProtocol: GetUserByIdProtocol, ) {} async execute(id: string, name: string): Promise { const exists = await this.getByIdProtocol.getById(id); if (!exists) { throw new UserNotExistsException(`User with id ${id} not exists`); } return this.updateProtocol.update(id, name); } } ``` #### Deleção ```typescript // core/features/DeleteUser.ts export abstract class DeleteUser { abstract execute(id: string): Promise; } // core/usecases/DeleteUserUsecase.ts export class DeleteUserUsecase implements DeleteUser { constructor( private readonly deleteProtocol: DeleteUserProtocol, private readonly getByIdProtocol: GetUserByIdProtocol, ) {} async execute(id: string): Promise { const exists = await this.getByIdProtocol.getById(id); if (!exists) { throw new UserNotExistsException(`User with id ${id} not exists`); } return this.deleteProtocol.delete(id); } } ``` #### Exception e protocols restantes ```typescript export class UserNotExistsException extends IBaseException { constructor(message: string) { super(message); this.code = 404; } } ``` ```typescript // core/protocols/GetUserByIdProtocol.ts export abstract class GetUserByIdProtocol { abstract getById(id: string): Promise; } // core/protocols/UpdateUserProtocol.ts export abstract class UpdateUserProtocol { abstract update(id: string, name: string): Promise; } // core/protocols/DeleteUserProtocol.ts export abstract class DeleteUserProtocol { abstract delete(id: string): Promise; } ``` ### Conclusão Separar Core, Adapters e Infra deixa a regra de negócio testável sem Nest, Prisma ou HTTP. Trocar banco ou canal de entrada vira troca de adapter e wiring de DI — não reescrita do usecase. O custo é mais arquivos e disciplina de fronteira; o ganho aparece quando o sistema precisa mudar sem arrastar o domínio junto. Publicado originalmente no [Medium](https://medium.com/@contato.dev.rafael.pereira/typescript-cleanarch-668935d677c2) (15/03/2023). --- # Flapper: discovering the domain before splitting the monolith Source: cases/flapper-modernizacao-en.md > Incremental modernization of an undocumented PHP monolith at Flapper: database as discovery source, bounded contexts, and Strangler. The domain-based split reduced the table count by approximately 25%. ### Context At Flapper, the core product was a PHP monolith over seven years old, with little useful documentation and without the developers who had created it. The application sustained the executive aviation operation and could not be stopped for a rewrite. ### Constraints Code and database accumulated rules and dependencies that were hard to explain. Changes had unpredictable side effects, and there were no remaining specialists to confirm how each part of the system should evolve. Migration had to coexist with the product in production. ### Problem Switching PHP for another technology would not answer the main question: which rules belonged together and which dependencies could be separated without breaking operations. The system needed domain boundaries before new services. ### Decision I used the existing database and code as a discovery source. Table groupings and relationships helped identify bounded contexts; from there, migration followed the Strangler pattern: 1. extract one domain at a time, without stopping the monolith; 2. keep in each context only the local representation of the data it needed; 3. propagate changes through Kafka events, instead of connecting every service to the old database; 4. use Node.js, NestJS, and Go in the first modules, with gRPC, REST, or GraphQL depending on the consumer; 5. document the strategy and the first modules so the team could continue the transformation. ### Discarded alternative Rewriting the whole monolith, or keeping new services tied to the same database and shared relationships. The first option would stop the business; the second would preserve the coupling the migration needed to reduce. ### Result The domain-based split reduced the table count by approximately 25% and created seven databases organized by context. The people, authentication, and aircraft modules were the first steps of a transformation planned to continue beyond the initial delivery. ### Limitations The number measures the table reduction in that domain split, not financial gain, the complete migration, or a market result for the company. The experience end date diverges across older sources; the published period follows the exported profile. If a domain still depends on rules unmapped in the monolith, its extraction must be postponed or given an explicit transitional integration. --- # Flapper: descubrir el dominio antes de separar el monolito Source: cases/flapper-modernizacao-es.md > Modernización incremental de un monolito PHP sin documentación en Flapper: base como fuente de descubrimiento, bounded contexts y Strangler. La separación por dominios redujo en aproximadamente el 25% el número de tablas. ### Contexto En Flapper, el producto principal era un monolito PHP con más de siete años, poca documentación útil y sin los desarrolladores que lo habían creado. La aplicación sostenía la operación de aviación ejecutiva y no podía interrumpirse para una reescritura. ### Restricciones El código y la base acumulaban reglas y dependencias difíciles de explicar. Los cambios tenían efectos colaterales poco predecibles, y no había especialistas remanentes para confirmar cómo cada parte del sistema debía evolucionar. La migración necesitaba coexistir con el producto en producción. ### Problema Cambiar PHP por otra tecnología no respondería la duda principal: qué reglas pertenecían juntas y qué dependencias podían separarse sin romper la operación. El sistema necesitaba fronteras de dominio antes de servicios nuevos. ### Decisión Usé la base y el código existente como fuente de descubrimiento. Agrupamientos de tablas y relaciones ayudaron a identificar bounded contexts; a partir de ellos, la migración siguió el patrón Strangler: 1. extraer un dominio por vez, sin interrumpir el monolito; 2. mantener en cada contexto solo la representación local de los datos que necesitaba; 3. propagar cambios por eventos en Kafka, en lugar de conectar todos los servicios a la base antigua; 4. usar Node.js, NestJS y Go en los primeros módulos, con gRPC, REST o GraphQL según el consumidor; 5. documentar la estrategia y los primeros módulos para que el equipo pudiera continuar la transformación. ### Alternativa descartada Reescribir el monolito entero, o mantener servicios nuevos atados a la misma base y a las mismas relaciones compartidas. La primera opción pararía el negocio; la segunda preservaría el acoplamiento que la migración necesitaba reducir. ### Resultado La separación por dominios redujo en aproximadamente el 25% el número de tablas y creó siete bases organizadas por contexto. Los módulos de personas, autenticación y aeronaves fueron los primeros pasos de una transformación planeada para continuar más allá de la entrega inicial. ### Limitaciones El número mide la reducción de tablas en esa separación de dominios, no una ganancia financiera, la migración completa o un resultado de mercado de la empresa. La fecha final de la experiencia tiene divergencia histórica en fuentes antiguas; el período publicado sigue el perfil exportado. Si un dominio aún depende de reglas no mapeadas en el monolito, su extracción debe posponerse o recibir una integración de transición explícita. --- # Flapper: descobrir o domínio antes de separar o monólito Source: cases/flapper-modernizacao-pt-br.md > Modernização incremental de um monólito PHP sem documentação na Flapper: banco como fonte de descoberta, bounded contexts e Strangler. A separação por domínios reduziu em aproximadamente 25% o número de tabelas. ### Contexto Na Flapper, o produto principal era um monólito PHP com mais de sete anos, pouca documentação útil e sem os desenvolvedores que o haviam criado. A aplicação sustentava a operação de aviação executiva e não podia ser interrompida para uma reescrita. ### Restrições O código e o banco acumulavam regras e dependências difíceis de explicar. Alterações tinham efeitos colaterais pouco previsíveis, e não havia especialistas remanescentes para confirmar como cada parte do sistema deveria evoluir. A migração precisava coexistir com o produto em produção. ### Problema Trocar PHP por outra tecnologia não responderia à dúvida principal: quais regras pertenciam juntas e quais dependências poderiam ser separadas sem quebrar a operação. O sistema precisava de fronteiras de domínio antes de serviços novos. ### Decisão Usei o banco e o código existente como fonte de descoberta. Agrupamentos de tabelas e relações ajudaram a identificar bounded contexts; a partir deles, a migração seguiu o padrão Strangler: 1. extrair um domínio por vez, sem interromper o monólito; 2. manter em cada contexto apenas a representação local dos dados de que precisava; 3. propagar mudanças por eventos em Kafka, em vez de conectar todos os serviços ao banco antigo; 4. usar Node.js, NestJS e Go nos primeiros módulos, com gRPC, REST ou GraphQL conforme o consumidor; 5. documentar a estratégia e os primeiros módulos para que o time pudesse continuar a transformação. ### Alternativa descartada Reescrever o monólito inteiro, ou manter serviços novos presos ao mesmo banco e às mesmas relações compartilhadas. A primeira opção pararia o negócio; a segunda preservaria o acoplamento que a migração precisava reduzir. ### Resultado A separação por domínios reduziu em aproximadamente 25% o número de tabelas e criou sete bancos organizados por contexto. Os módulos de pessoas, autenticação e aeronaves foram os primeiros passos de uma transformação planejada para continuar além da entrega inicial. ### Limitações O número mede a redução de tabelas nessa separação de domínios, não um ganho financeiro, a migração completa ou um resultado de mercado da empresa. A data final da experiência tem divergência histórica em fontes antigas; o período publicado segue o perfil exportado. Se um domínio ainda depender de regras não mapeadas no monólito, sua extração precisa ser adiada ou receber uma integração de transição explícita. --- # Infosistemas: a failure contract for RabbitMQ messaging Source: cases/infosistemas-mensageria-en.md > RabbitMQ messaging redesign at Infosistemas with durable queues, DLQ, retry, idempotency, and prefetch. The work reduced intermittent failures between microservices by about 98%. ### Context Infosistemas operates management platforms for rental companies, fleets, and automakers. The work happened in the architecture team, in collaboration with DevOps, SREs, and DBAs, between February 2025 and May 2026. This case covers the messaging track. Other tracks from the same experience (ERP security, signing journeys, fiscal integrations) exist in the sources but are not included here as extra numbers or claims. ### Constraints Critical flows crossed microservices. Failure was intermittent: the same operation could complete on one run and fail on the next. Raising concurrency or prefetch without criteria transferred overload to consumers, services, or downstream databases. ### Problem Messages stopped completing the expected flow. Investigating partial failure was hard. There was no explicit contract for temporary failure, permanent failure, duplication, or poison messages. ### Decision Messaging was redesigned to make failure behavior predictable: - durable queues; - per-flow DLQ, so a message that must neither disappear nor repeat without control has a place to go; - retry with backoff for temporary unavailability; - idempotency and deduplication in the consumer, because duplicated delivery must not repeat a business effect; - publisher confirms, to reduce uncertainty at publish time; - prefetch tuning, instead of opening concurrency indiscriminately. ### Discarded alternative Treating the problem as a capacity shortage (more consumers, more prefetch) without changing the failure contract. That would move the bottleneck and keep silent loss or duplication. ### Implementation The redesign applied these mechanisms to the critical flows: confirmed publishing, durable queues with controlled prefetch, idempotent consumers, retry with backoff, and per-flow DLQ. Operations gained a predictable path for temporary failure and for permanent failure, instead of relying on ad hoc reprocessing. ### Result The recorded reduction in intermittent failures in critical flows between microservices was approximately 98%. The number describes those flows after the redesign, not the entire company operation nor other tracks. ### Limitations Metrics from other tracks that are still pending method or confirmation are left out. If volume or the microservice map changes so that DLQ and prefetch no longer isolate failure, tuning must be revisited with queue and consumer telemetry. --- # Infosistemas: contrato de fallo en la mensajería RabbitMQ Source: cases/infosistemas-mensageria-es.md > Rediseño de la mensajería RabbitMQ en Infosistemas con colas durables, DLQ, retry, idempotencia y prefetch. El trabajo redujo en cerca del 98% los fallos intermitentes entre microservicios. ### Contexto Infosistemas opera plataformas de gestión para arrendadoras, flotas y automotrices. El trabajo ocurrió en el equipo de arquitectura, en colaboración con DevOps, SREs y DBAs, entre febrero de 2025 y mayo de 2026. Este caso cubre el frente de mensajería. Otros frentes de la misma experiencia (seguridad del ERP, jornadas de firma, integraciones fiscales) existen en las fuentes, pero no entran aquí como número o afirmación extra. ### Restricciones Los flujos críticos cruzaban microservicios. El fallo era intermitente: la misma operación podía completarse en una ejecución y no completarse en la siguiente. Aumentar concurrencia o prefetch sin criterio transfería sobrecarga a consumidores, servicios o bases downstream. ### Problema Los mensajes dejaban de completar el flujo esperado. Investigar un fallo parcial era difícil. No había contrato explícito para fallo temporal, fallo permanente, duplicidad o poison message. ### Decisión La mensajería fue rediseñada para volver predecible el comportamiento ante fallos: - colas durables; - DLQ por flujo, para el mensaje que no debe desaparecer ni repetirse sin control; - retry con backoff para indisponibilidad temporal; - idempotencia y deduplicación en el consumidor, porque la entrega duplicada no puede repetir el efecto de negocio; - publisher confirms, para reducir la incertidumbre en la publicación; - ajuste de prefetch, en lugar de abrir concurrencia indiscriminada. ### Alternativa descartada Tratar el problema como falta de capacidad (más consumidores, más prefetch) sin cambiar el contrato de fallo. Eso movería el cuello de botella y mantendría pérdida o duplicidad silenciosa. ### Implementación El rediseño colocó estos mecanismos en los flujos críticos: publicación confirmada, cola durable con prefetch controlado, consumidor idempotente, retry con backoff y DLQ por flujo. La operación pasó a tener un camino predecible para el fallo temporal y para el fallo permanente, en lugar de depender de reprocesamiento ad hoc. ### Resultado La reducción registrada en los fallos intermitentes de los flujos críticos entre microservicios fue de aproximadamente el 98%. El número describe esos flujos tras el rediseño, no la operación entera de la empresa ni otros frentes. ### Limitaciones Las métricas de otros frentes aún pendientes de método o confirmación quedan fuera. Si la volumetría o el mapa de microservicios cambia de forma que DLQ y prefetch dejen de aislar el fallo, el tuning debe revisarse con telemetría de colas y de consumidores. --- # Infosistemas: contrato de falha na mensageria RabbitMQ Source: cases/infosistemas-mensageria-pt-br.md > Redesenho da mensageria RabbitMQ na Infosistemas com filas duráveis, DLQ, retry, idempotência e prefetch. O trabalho reduziu em cerca de 98% as falhas intermitentes entre microsserviços. ### Contexto A Infosistemas opera plataformas de gestão para locadoras, frotas e montadoras. O trabalho ocorreu no time de arquitetura, em colaboração com DevOps, SREs e DBAs, entre fevereiro de 2025 e maio de 2026. Este caso cobre a frente de mensageria. Outras frentes da mesma experiência (segurança de ERP, jornadas de assinatura, integrações fiscais) existem nas fontes, mas não entram aqui como número ou afirmação extra. ### Restrições Os fluxos críticos cruzavam microsserviços. A falha era intermitente: a mesma operação podia completar numa execução e não completar na seguinte. Aumentar concorrência ou prefetch sem critério transferia sobrecarga para consumidores, serviços ou bancos downstream. ### Problema Mensagens deixavam de completar o fluxo esperado. Investigar falha parcial era difícil. Não havia contrato explícito para falha temporária, falha permanente, duplicidade ou poison message. ### Decisão A mensageria foi redesenhada para tornar o comportamento em falha previsível: - filas duráveis; - DLQ por fluxo, para mensagem que não deve desaparecer nem repetir sem controle; - retry com backoff para indisponibilidade temporária; - idempotência e deduplicação no consumidor, porque entrega duplicada não pode repetir efeito de negócio; - publisher confirms, para reduzir incerteza na publicação; - ajuste de prefetch, em vez de abrir concorrência indiscriminada. ### Alternativa descartada Tratar o problema como falta de capacidade (mais consumidores, mais prefetch) sem mudar o contrato de falha. Isso moveria o gargalo e manteria perda ou duplicidade silenciosa. ### Implementação O redesenho colocou esses mecanismos nos fluxos críticos: publicação confirmada, fila durável com prefetch controlado, consumidor idempotente, retry com backoff e DLQ por fluxo. A operação passou a ter um caminho previsível para falha temporária e para falha permanente, em vez de depender de reprocessamento ad hoc. ### Resultado A redução registrada nas falhas intermitentes dos fluxos críticos entre microsserviços foi de aproximadamente 98%. O número descreve esses fluxos após o redesenho, não a operação inteira da empresa nem outras frentes. ### Limitações Métricas de outras frentes ainda pendentes de método ou confirmação ficam de fora. Se a volumetria ou o mapa de microsserviços mudar de forma que DLQ e prefetch deixem de isolar a falha, o tuning precisa ser revisto com telemetria de fila e de consumidores. --- # VBET: analytics on top of a SQL Server we could not change Source: cases/vbet-analytics-en.md > Commission dashboard at VBET: external SQL Server, owned ETL, and cache. The measured line went from about seven minutes at peak to under one second with a warm cache. ### Context At VBET, between October 2023 and February 2025, the analytics product served iGaming influencers and affiliates. The dashboard gathered dozens of metrics; commission was the most critical reading. Influencers accepted a small lag in same-day data as long as the screen responded. Payout depended on consolidated prior-day data, not on the live value. ### Constraints The SQL Server was external, shared, and not reliably modifiable. Temporary indexes could be removed by the database owner. The original API mixed SQL queries built from parameters, with injection risk, and aggregated too much in memory. ### Problem The system had been sized for smaller influencers. With larger bases, the worst dashboard peak reached about seven minutes. Security and maintainability came before performance: raw queries, little test coverage, and a synchronous path that recalculated too much on every request. ### Decision The evolution was incremental, in the order the constraints appeared: 1. remove unsafe SQL, parameterize access, document, and test; 2. optimize queries and temporary indexes, as mitigation rather than invariant; 3. parallelize independent queries with Go, goroutines, and channels; 4. when the bottleneck moved back to SQL Server, build an owned ETL and PostgreSQL, with pre-computation, checkpoints, and reconciliation; 5. separate REALTIME reads (trend, eventual consistency) from CLOSED reads (financial accuracy and payout); 6. cache-aside with TTL aligned to the accepted lag of about five minutes; 7. controlled degradation if the cache failed, instead of taking the screen down. ### Discarded alternative Insisting on indexes in the external database as architecture, or recalculating years of history on every access. Also discarded: paying affiliates from REALTIME data. ### Result The reconciled line in the experience dossier, for the commission/dashboard path, is: - initial worst peak: about 7 minutes; - after queries and indexes: about 3 minutes; - after parallelization: about 1 minute; - after ETL/PostgreSQL: about 15 seconds at commission p99 without cache; - warm cache: under 1 second. Each number belongs to its stage. It does not describe the gain of a later decomposition into microservices. ### Limitations The investigation, the ETL + owned database decision, the REALTIME/CLOSED split, and the degradation policy are the attributable core here. Decomposing the monolith into Kubernetes is a separate story and does not mix the "500%" nor sub-60 ms latency into this case. Older resume versions citing 30 seconds on cold load, 11 seconds, or SLA percentages without a scenario are left out. If the product starts requiring realtime accuracy for payout, the CLOSED split stops being enough. --- # VBET: analítica sobre un SQL Server que no podíamos cambiar Source: cases/vbet-analytics-es.md > Dashboard de comisiones en VBET: SQL Server externo, ETL propio y caché. La línea medida fue de cerca de siete minutos en el pico hasta menos de un segundo con caché caliente. ### Contexto En VBET, entre octubre de 2023 y febrero de 2025, el producto de analítica servía a influencers y afiliados de iGaming. El dashboard reunía decenas de métricas; la comisión era la lectura más crítica. Los influencers aceptaban un pequeño desfase en los datos del día, siempre que la pantalla respondiera. El pago dependía de datos consolidados del día anterior, no del valor en vivo. ### Restricciones El SQL Server era externo, compartido y no modificable de forma fiable. Los índices temporales podían ser eliminados por el propietario de la base. La API original mezclaba consultas SQL construidas a partir de parámetros, con riesgo de inyección, y agregaba demasiado en memoria. ### Problema El sistema había sido dimensionado para influencers más pequeños. Con bases mayores, el peor pico del dashboard llegó a cerca de siete minutos. Seguridad y mantenibilidad vinieron antes del rendimiento: queries crudas, poca cobertura de pruebas y un camino síncrono que recalculaba demasiado en cada request. ### Decisión La evolución fue incremental, en el orden en que aparecieron las restricciones: 1. eliminar SQL inseguro, parametrizar el acceso, documentar y probar; 2. optimizar queries e índices temporales, como mitigación y no como invariante; 3. paralelizar consultas independientes con Go, goroutines y channels; 4. cuando el cuello de botella volvió al SQL Server, crear ETL y PostgreSQL propios, con precálculo, checkpoints y reconciliación; 5. separar lectura REALTIME (tendencia, consistencia eventual) de CLOSED (precisión financiera y pago); 6. cache-aside con TTL alineado al desfase aceptado de cerca de cinco minutos; 7. degradación controlada si fallaba la caché, en lugar de tumbar la pantalla. ### Alternativa descartada Insistir en índices en la base externa como arquitectura, o recalcular años de historial en cada acceso. También se descartó la idea de pagar al afiliado con el dato REALTIME. ### Resultado La línea reconciliada en el dosier de la experiencia, para el camino de comisión/dashboard, es: - peor pico inicial: cerca de 7 minutos; - tras queries e índices: cerca de 3 minutos; - tras paralelización: cerca de 1 minuto; - tras ETL/PostgreSQL: cerca de 15 segundos en el p99 de la comisión sin caché; - caché caliente: menos de 1 segundo. Cada número pertenece a esa etapa. No describe la ganancia de una descomposición posterior en microservicios. ### Limitaciones La investigación, la decisión de ETL + base propia, la separación REALTIME/CLOSED y la política de degradación son el núcleo atribuible aquí. La descomposición del monolito en Kubernetes es otra historia y no mezcla el "500%" ni la latencia por debajo de 60 ms con este caso. Versiones antiguas de currículo que citan 30 segundos en carga fría, 11 segundos o porcentajes de SLA sin escenario quedan fuera. Si el producto pasa a exigir precisión realtime en el pago, la separación CLOSED deja de ser suficiente. --- # VBET: analytics sobre um SQL Server que não podíamos mudar Source: cases/vbet-analytics-pt-br.md > Dashboard de comissões na VBET: SQL Server externo, ETL próprio e cache. A linha medida foi de cerca de sete minutos no pico até menos de um segundo com cache quente. ### Contexto Na VBET, entre outubro de 2023 e fevereiro de 2025, o produto de analytics servia influenciadores e afiliados de iGaming. O dashboard reunia dezenas de métricas; comissão era a leitura mais crítica. Influenciadores aceitavam pequena defasagem nos dados do dia, desde que a tela respondesse. Pagamento dependia de dados consolidados do dia anterior, não do valor ao vivo. ### Restrições O SQL Server era externo, compartilhado e não modificável de forma confiável. Índices temporários podiam ser removidos pelo proprietário da base. A API original misturava consultas SQL montadas a partir de parâmetros, com risco de injeção, e agregava demais em memória. ### Problema O sistema havia sido dimensionado para influenciadores menores. Com bases maiores, o pior pico do dashboard chegou a cerca de sete minutos. Segurança e manutenibilidade vieram antes da performance: queries cruas, pouca cobertura de testes e um caminho síncrono que recalculava demais a cada request. ### Decisão A evolução foi incremental, na ordem em que as restrições apareceram: 1. remover SQL inseguro, parametrizar acesso, documentar e testar; 2. otimizar queries e índices temporários, como mitigação e não como invariante; 3. paralelizar consultas independentes com Go, goroutines e channels; 4. quando o gargalo voltou para o SQL Server, criar ETL e PostgreSQL próprios, com pré-cálculo, checkpoints e reconciliação; 5. separar leitura REALTIME (tendência, consistência eventual) de CLOSED (precisão financeira e pagamento); 6. cache-aside com TTL alinhado à defasagem aceita de cerca de cinco minutos; 7. degradação controlada se o cache falhasse, em vez de derrubar a tela. ### Alternativa descartada Insistir em índices na base externa como arquitetura, ou recalcular anos de histórico a cada acesso. Também descartada a ideia de pagar o afiliado com o dado REALTIME. ### Resultado A linha reconciliada no dossiê da experiência, para o caminho de comissão/dashboard, é: - pior pico inicial: cerca de 7 minutos; - após queries e índices: cerca de 3 minutos; - após paralelização: cerca de 1 minuto; - após ETL/PostgreSQL: cerca de 15 segundos no p99 da comissão sem cache; - cache quente: menos de 1 segundo. Cada número pertence a essa etapa. Não descreve o ganho de uma decomposição posterior em microsserviços. ### Limitações A investigação, a decisão de ETL + base própria, a separação REALTIME/CLOSED e a política de degradação são o núcleo atribuível aqui. A decomposição do monólito em Kubernetes é outra história e não mistura o “500%” nem a latência abaixo de 60 ms com este caso. Versões antigas de currículo que citam 30 segundos em carga fria, 11 segundos ou percentuais de SLA sem cenário ficam de fora. Se o produto passar a exigir precisão realtime no pagamento, a separação CLOSED deixa de ser suficiente. --- # Azify | Rafael Pereira Source: experiences/azify-en.md > My consulting work at Azify with settlement, BaaS, and financial services. ### Context At Azify, I worked as a consultant on financial infrastructure for fintechs and smaller banks. The product covered capabilities such as Pix, transfers, cards, digital wallets, and other banking services. ### How I worked I contributed to architectural decisions and helped establish engineering practices for an environment where consistency, security, and stability had direct financial impact. I built a NestJS settlement engine with multi-exchange integrations, risk monitoring, and compliance controls. I also worked on exchange and blockchain integrations for transactional flows and on a multi-tenant BaaS platform using OAuth 2.0, JWT, and encryption. Through query profiling, index review, and Redis optimization, I reduced latency in critical financial APIs by 30%. ### What I took from it This consulting work brought recurring parts of my trajectory, including payments, multi-tenancy, and transactional systems, into a setting with even greater responsibility for authorization and consistency. --- # Azify | Rafael Pereira Source: experiences/azify-es.md > Mi consultoría en Azify con liquidación, BaaS y servicios financieros. ### Contexto En Azify, trabajé como consultor en infraestructura financiera para fintechs y bancos pequeños. El producto reunía capacidades como Pix, transferencias, tarjetas, billetera digital y otros servicios bancarios. ### Cómo trabajé Participé en decisiones arquitectónicas y ayudé a establecer prácticas de desarrollo para un entorno donde consistencia, seguridad y estabilidad tenían impacto financiero directo. Desarrollé un motor de liquidación en NestJS con integraciones multi-exchange, monitoreo de riesgo y controles de compliance. También trabajé en integraciones de exchanges y blockchains para flujos transaccionales y en una plataforma BaaS multi-tenant con OAuth 2.0, JWT y cifrado. Mediante profiling de consultas, revisión de índices y optimización de Redis, reduje en 30% la latencia de APIs financieras críticas. ### Lo que me llevé Esta consultoría retomó partes recurrentes de mi trayectoria, como pagos, multi-tenancy y sistemas transaccionales, en un contexto con mayor responsabilidad sobre autorización y consistencia. --- # Azify | Rafael Pereira Source: experiences/azify-pt-br.md > Minha consultoria na Azify com liquidação, BaaS e serviços financeiros. ### O contexto Na Azify, atuei como consultor em infraestrutura financeira para fintechs e pequenos bancos. O produto reunia capacidades como Pix, transferências, cartões, carteira digital e outros serviços bancários. ### Como atuei Participei de decisões arquiteturais e ajudei a estruturar práticas de desenvolvimento para um ambiente em que consistência, segurança e estabilidade tinham impacto financeiro direto. Desenvolvi um motor de liquidação em NestJS com integrações a múltiplas exchanges, monitoramento de risco e controles de compliance. Também trabalhei na integração de exchanges e blockchains aos fluxos transacionais e na evolução de uma plataforma BaaS multi-tenant com OAuth 2.0, JWT e criptografia. Com profiling de consultas, revisão de índices e ajuste do uso de Redis, reduzi em 30% a latência de APIs financeiras críticas. ### O que levo Essa consultoria retomou temas recorrentes da minha trajetória, como pagamentos, multi-tenancy e sistemas transacionais, com um grau de responsabilidade ainda maior sobre autorização e consistência. --- # Braistech | Rafael Pereira Source: experiences/braistech-en.md > My experience at Braistech with product, microservices, and crypto assets. ### Context At Braistech, I had one of my first product experiences in a small environment with a distributed level of responsibility. The domain involved crypto-asset contracts and financial movements. ### How I worked I led the structure of the main system with Node.js and NestJS, took part in designing microservices for the business core, and developed Flutter applications. I also built a contract system and worked on payment integrations related to the Binance ecosystem. I participated in a transition from an MVC organization toward Clean Architecture. The goal was to reduce coupling and make a growing system easier to maintain, while I guided junior developers through the code decisions. ### What I took from it This stage consolidated my interest in backend and architecture. Working across the full product also gave me a full-stack perspective that remains useful in conversations with frontend and product teams. --- # Braistech | Rafael Pereira Source: experiences/braistech-es.md > Mi experiencia en Braistech con producto, microservicios y criptoactivos. ### Contexto En Braistech, tuve una de mis primeras experiencias de producto en un entorno pequeño, con pocas personas y responsabilidad distribuida. El dominio involucraba contratos de criptoactivos y movimientos financieros. ### Cómo trabajé Lideré la estructuración del sistema principal con Node.js y NestJS, participé en el diseño de microservicios para el núcleo del negocio y desarrollé aplicaciones Flutter. También construí un sistema de contratos y trabajé en integraciones de pago relacionadas con el ecosistema de Binance. Participé además en la transición de una organización MVC hacia Clean Architecture. El objetivo era reducir acoplamiento y facilitar el mantenimiento de un sistema en crecimiento, mientras orientaba a desarrolladores junior en las decisiones de código. ### Lo que me llevé Esta etapa consolidó mi interés por backend y arquitectura. Trabajar en todo el producto también me dio una visión full stack que sigue siendo útil en conversaciones con frontend y producto. --- # Braistech | Rafael Pereira Source: experiences/braistech-pt-br.md > Minha experiência na Braistech com produto, microsserviços e criptoativos. ### O contexto Na Braistech, vivi uma das minhas primeiras experiências de produto em um ambiente pequeno, com pouca gente e responsabilidade distribuída. O domínio envolvia contratos de criptoativos e movimentações financeiras. ### Como atuei Liderei a estruturação do sistema principal com Node.js e NestJS, participei do desenho de microsserviços para o núcleo do negócio e desenvolvi aplicações em Flutter. Também construí um sistema de contratos e trabalhei com integrações de pagamento relacionadas ao ecossistema da Binance. Participei ainda da transição de uma organização baseada em MVC para uma arquitetura mais próxima de Clean Architecture. O objetivo era reduzir acoplamento e facilitar a manutenção de um sistema em crescimento, enquanto eu orientava desenvolvedores juniores nas decisões do código. ### O que levo Essa etapa consolidou meu interesse por backend e arquitetura. O contato com produto completo também me deu uma visão full stack que segue útil nas conversas com frontend e produto. --- # EDS and Rio de Janeiro Civil Police | Rafael Pereira Source: experiences/eds-policia-civil-rio-en.md > My consulting experience on sensitive public systems. ### Context In my consulting work for EDS, I worked on systems for the Civil Police of Rio de Janeiro. The context involved a critical public operation, a high-volume health management system, and an evolving legal ERP. ### How I worked I structured the health system backend with NestJS and SQL Server. I also refactored legacy routes and contributed to flows for process automation, document management, and evidence collection. Security, access control, traceability, and LGPD compliance guided how every route had to evolve. Beyond backend work, I collaborated on shared design-system components to align API contracts with the interfaces used in the operation. ### What I took from it The work reinforced the care needed to evolve sensitive systems without losing auditability. Rather than separating security from delivery, I treated access and traceability as part of the product contract. --- # EDS y Policía Civil de Río de Janeiro | Rafael Pereira Source: experiences/eds-policia-civil-rio-es.md > Mi experiencia de consultoría en sistemas públicos sensibles. ### Contexto En mi consultoría para EDS, trabajé en sistemas destinados a la Policía Civil de Río de Janeiro. El contexto incluía una operación pública crítica, un sistema de gestión de salud de alto volumen y un ERP jurídico en evolución. ### Cómo trabajé Estructuré el backend del sistema de salud con NestJS y SQL Server. También refactoricé rutas legadas y participé en flujos de automatización de procesos, gestión documental y recolección de evidencias. Seguridad, control de acceso, trazabilidad y cumplimiento de LGPD orientaban cómo debía evolucionar cada ruta. Además del backend, colaboré en componentes compartidos del design system para alinear contratos de API con las interfaces usadas en la operación. ### Lo que me llevé El trabajo reforzó el cuidado necesario para evolucionar sistemas sensibles sin perder auditabilidad. En lugar de separar seguridad y entrega, traté acceso y trazabilidad como parte del contrato del producto. --- # EDS e Polícia Civil do Rio de Janeiro | Rafael Pereira Source: experiences/eds-policia-civil-rio-pt-br.md > Minha experiência de consultoria em sistemas públicos sensíveis. ### O contexto Na consultoria para a EDS, trabalhei em sistemas destinados à Polícia Civil do Rio de Janeiro. O contexto envolvia uma operação pública crítica, um sistema de gestão de saúde de alto volume e a evolução de um ERP jurídico. ### Como atuei Estruturei o backend do sistema de saúde com NestJS e SQL Server. Também refatorei rotas legadas e participei do desenho de fluxos para automação de processos, gestão documental e coleta de evidências. Segurança, controle de acesso, rastreabilidade e LGPD não eram requisitos isolados: orientavam como cada rota precisava evoluir. Além do backend, colaborei com a manutenção de componentes compartilhados do design system para alinhar contratos de API e o comportamento das interfaces usadas na operação. ### O que levo Foi uma experiência que reforçou o cuidado necessário para evoluir sistemas sensíveis sem perder auditabilidade. Em vez de separar segurança da entrega, tratei acesso e rastreabilidade como parte do contrato do produto. --- # Flapper | Rafael Pereira Source: experiences/flapper-en.md > My experience at Flapper with incremental modernization of a production legacy system. ### Context At Flapper, I worked on an executive aviation platform whose main product was a PHP monolith more than seven years old, with little useful documentation and no original developers available to explain the system. ### How I worked The challenge was not to replace PHP with TypeScript. The application was in production and sustained the business, so I began with domain discovery. I used the database to map relationships, identify bounded contexts, and plan an incremental migration based on the Strangler pattern. I migrated modules such as people, authentication, and aircraft to Node.js, NestJS, and Go services. To reduce relational dependencies between contexts, we worked with local projections and Kafka events; gRPC, REST, and GraphQL were used according to each integration's needs. ### What I took from it The experience consolidated my view of legacy modernization: technology comes after understanding boundaries, risks, and a sequence that preserves the operation. Alongside module delivery, I documented decisions and led workshops so the team could continue the transformation. --- # Flapper | Rafael Pereira Source: experiences/flapper-es.md > Mi experiencia en Flapper con modernización incremental de un legado en producción. ### Contexto En Flapper, trabajé en una plataforma de aviación ejecutiva cuyo producto principal era un monolito PHP de más de siete años, con poca documentación útil y sin desarrolladores originales disponibles para explicar el sistema. ### Cómo trabajé El desafío no era reemplazar PHP por TypeScript. La aplicación estaba en producción y sostenía el negocio, así que empecé por descubrir el dominio. Usé la base de datos para mapear relaciones, identificar bounded contexts y planificar una migración incremental basada en el patrón Strangler. Migré módulos como personas, autenticación y aeronaves a servicios en Node.js, NestJS y Go. Para reducir dependencias relacionales entre contextos, trabajamos con proyecciones locales y eventos en Kafka; gRPC, REST y GraphQL se usaron según la necesidad de cada integración. ### Lo que me llevé Esta experiencia consolidó mi visión de modernización de legado: la tecnología viene después de entender fronteras, riesgos y una secuencia que preserve la operación. Además de entregar módulos, documenté decisiones y conduje workshops para que el equipo continuara la transformación. --- # Flapper | Rafael Pereira Source: experiences/flapper-pt-br.md > Minha experiência na Flapper com modernização incremental de um legado em produção. ### O contexto Na Flapper, trabalhei em uma plataforma de aviação executiva cujo produto principal era um monólito PHP com mais de sete anos, pouca documentação útil e sem os desenvolvedores originais disponíveis para explicar o sistema. ### Como atuei O desafio não era trocar PHP por TypeScript. A aplicação estava em produção e sustentava o negócio, então comecei pela descoberta do domínio. Usei o banco de dados para mapear relações, identificar bounded contexts e planejar uma migração incremental baseada no padrão Strangler. Migrei módulos como pessoas, autenticação e aeronaves para serviços em Node.js, NestJS e Go. Para reduzir dependências relacionais entre contextos, trabalhamos com projeções locais e eventos no Kafka; gRPC, REST e GraphQL foram aplicados conforme a necessidade de cada integração. ### O que levo Foi uma experiência que consolidou minha visão de modernização de legado: tecnologia vem depois de entender fronteiras, riscos e uma sequência que preserve a operação. Além de entregar módulos, documentei decisões e conduzi workshops para que o time pudesse continuar a transformação. --- # Infosistemas | Rafael Pereira Source: experiences/infosistemas-en.md > My experience at Infosistemas with messaging, integrations, and mobility platforms. ### Context At Infosistemas, I worked on platforms for rental companies, fleets, automakers, and mobility operations. It was an enterprise environment of integrations, fiscal flows, and high-volume services, where I worked closely with DevOps, SREs, and DBAs. ### How I worked My work combined architecture with hands-on delivery. I redesigned flows between microservices, led NestJS and Go integrations, and implemented critical-event traceability with NestJS and MongoDB. I also evolved APIs, investigated security issues, and contributed to digital journeys and webapp components when backend and interface continuity was needed. The principle that guided my work was making failures observable and manageable from the design stage. In messaging, I treated durability, retry, idempotency, and consumer control as part of the flow rather than later fixes. ### What I took from it This experience expanded my work in systems with many dependencies and specialists. I learned to turn requirements, risks, and operational constraints into decisions that stayed clear through client validation. --- # Infosistemas | Rafael Pereira Source: experiences/infosistemas-es.md > Mi experiencia en Infosistemas con mensajería, integraciones y plataformas de movilidad. ### Contexto En Infosistemas, trabajé en plataformas para arrendadoras, flotas, automotrices y operaciones de movilidad. Era un entorno enterprise de integraciones, flujos fiscales y servicios de alto volumen, donde trabajé junto a DevOps, SREs y DBAs. ### Cómo trabajé Mi trabajo combinó arquitectura y ejecución práctica. Rediseñé flujos entre microservicios, lideré integraciones en NestJS y Go e implementé trazabilidad de eventos críticos con NestJS y MongoDB. También evolucioné APIs, investigué problemas de seguridad y contribuí a jornadas digitales y componentes web cuando era necesaria la continuidad entre backend e interfaz. El principio que orientó mi trabajo fue hacer observables y tratables las fallas desde el diseño. En mensajería, traté durabilidad, retry, idempotencia y control de consumo como partes del flujo, no como correcciones posteriores. ### Lo que me llevé Esta experiencia amplió mi trabajo en sistemas con muchas dependencias y especialistas. Aprendí a convertir requisitos, riesgos y restricciones operativas en decisiones que siguieran claras hasta la validación con el cliente. --- # Infosistemas | Rafael Pereira Source: experiences/infosistemas-pt-br.md > Minha experiência na Infosistemas com mensageria, integrações e plataformas de mobilidade. ### O contexto Na Infosistemas, atuei em plataformas para locadoras, frotas, montadoras e operações de mobilidade. Era um ambiente enterprise, com integrações, fluxos fiscais e serviços de grande volume, no qual trabalhei próximo de DevOps, SREs e DBAs. ### Como atuei Minha atuação combinou arquitetura e execução hands-on. Redesignei fluxos entre microsserviços, liderei integrações em NestJS e Go e implementei rastreabilidade de eventos críticos com NestJS e MongoDB. Também evoluí APIs, investiguei problemas de segurança e participei de jornadas digitais e de componentes do webapp quando a continuidade entre backend e interface era necessária. O ponto que mais orientou meu trabalho foi tornar falhas observáveis e tratáveis desde o desenho. Na mensageria, tratei durabilidade, retry, idempotência e controle de consumo como partes do fluxo, não como correções posteriores. ### O que levo Essa experiência ampliou minha atuação em sistemas com muitas dependências e especialistas envolvidos. Aprendi a transformar requisitos, riscos e restrições operacionais em decisões que continuassem claras até a validação com o cliente. --- # Maxmilhas | Rafael Pereira Source: experiences/maxmilhas-en.md > My experience at Maxmilhas with post-sale automation and legacy integration. ### Context At Maxmilhas, I worked in a short and intense period on travel post-sale flows involving cancellations, rebooking, coupons, and customer communication. ### How I worked I built Node.js, NestJS, and Elixir microservices to automate processes that still depended on support intervention. I implemented eligibility, expiry, and accumulation rules for coupons, along with calculations and validations for cancellation and rebooking flows. Together, those automations reduced the need for manual intervention by 34%. Another challenge was integrating a PHP 5.7 monolith, more than ten years old, with the commercial CRM without putting the operational core at risk. I also evolved flight monitoring, notifications, and proactive customer messages. ### What I took from it I learned to prioritize small, reversible interventions when outcomes need to arrive quickly. Instead of proposing a broad transformation, I focused on points that released operational work. --- # Maxmilhas | Rafael Pereira Source: experiences/maxmilhas-es.md > Mi experiencia en Maxmilhas con automatización posventa e integración de legado. ### Contexto En Maxmilhas, trabajé en un período corto e intenso, en flujos posventa de viajes relacionados con cancelaciones, cambios, cupones y comunicación con clientes. ### Cómo trabajé Desarrollé microservicios en Node.js, NestJS y Elixir para automatizar procesos que aún dependían de intervención del soporte. Implementé reglas de elegibilidad, expiración y acumulación de cupones, además de cálculos y validaciones para cancelaciones y cambios. En conjunto, esas automatizaciones redujeron en 34% la necesidad de intervención manual. Otro desafío fue integrar un monolito PHP 5.7, de más de diez años, al CRM comercial sin poner en riesgo el core de la operación. También evolucioné monitoreo de vuelos, notificaciones y mensajes proactivos. ### Lo que me llevé Aprendí a priorizar intervenciones pequeñas y reversibles cuando el resultado debía llegar rápido. En vez de proponer una transformación amplia, me concentré en puntos que liberaban trabajo operativo. --- # Maxmilhas | Rafael Pereira Source: experiences/maxmilhas-pt-br.md > Minha experiência na Maxmilhas com automação de pós-venda e legado. ### O contexto Na Maxmilhas, trabalhei em um período curto e intenso, em fluxos de pós-venda ligados a cancelamentos, remarcações, cupons e comunicação com clientes. ### Como atuei Desenvolvi microsserviços em Node.js, NestJS e Elixir para automatizar processos que ainda dependiam do suporte. Implementei regras de elegibilidade, expiração e cumulatividade de cupons, além de cálculos e validações necessários aos fluxos de cancelamento e remarcação. O conjunto dessas automações reduziu em 34% a necessidade de intervenção manual. Outro desafio foi integrar um monólito PHP 5.7, com mais de dez anos, ao CRM comercial sem colocar em risco o core da operação. Também evoluí monitoramento de voos, notificações e mensagens proativas. ### O que levo Aprendi a priorizar intervenções pequenas e reversíveis quando o resultado precisava aparecer rápido. Em vez de propor uma transformação ampla, concentrei a mudança nos pontos que liberavam trabalho operacional. --- # South System, QUIQ, and Itaú | Rafael Pereira Source: experiences/south-system-quiq-itau-en.md > My experience with a white-label, multi-tenant marketplace for financial institutions. ### Context At South System, I was assigned to QUIQ to work on a Marketplace as a Service for financial institutions. Itaú was the first context, but the product needed to accept new banks without requiring a fork for each client. ### How I worked I participated in architecture, data modeling, technology choices, and rule refinement with product. The result was a white-label, multi-tenant platform with logical tenant isolation and Hexagonal Architecture to keep the domain apart from specific integrations. I worked with Node.js, TypeScript, MySQL, asynchronous Go services, and AWS. I structured unit and integration testing for critical cases and used static analysis as part of the quality workflow. ### What I took from it This experience changed how I communicate architecture. I started treating alignment with product, the Product Owner, and stakeholders as part of the technical decision rather than a later step after code. --- # South System, QUIQ e Itaú | Rafael Pereira Source: experiences/south-system-quiq-itau-es.md > Mi experiencia con un marketplace white-label y multi-tenant para instituciones financieras. ### Contexto En South System, fui asignado a QUIQ para trabajar en un Marketplace as a Service para instituciones financieras. Itaú fue el primer contexto, pero el producto necesitaba incorporar nuevos bancos sin requerir un fork por cliente. ### Cómo trabajé Participé en arquitectura, modelado de datos, elección de tecnologías y refinamiento de reglas con producto. El resultado fue una plataforma white-label y multi-tenant, con aislamiento lógico entre tenants y Arquitectura Hexagonal para separar el dominio de las integraciones específicas. Trabajé con Node.js, TypeScript, MySQL, servicios asíncronos en Go y AWS. Estructuré pruebas unitarias y de integración para casos críticos y usé análisis estático como parte del flujo de calidad. ### Lo que me llevé Esta experiencia cambió cómo comunico arquitectura. Pasé a tratar la alineación con producto, Product Owner y stakeholders como parte de la decisión técnica, no como una etapa posterior al código. --- # South System, QUIQ e Itaú | Rafael Pereira Source: experiences/south-system-quiq-itau-pt-br.md > Minha experiência com marketplace white-label e multi-tenant para instituições financeiras. ### O contexto Na South System, fui alocado na QUIQ para trabalhar em um Marketplace as a Service voltado a instituições financeiras. O primeiro contexto era o Itaú, mas o produto precisava receber novos bancos sem exigir um fork por cliente. ### Como atuei Participei do desenho da arquitetura, da modelagem de banco, da escolha de tecnologias e do refinamento das regras com produto. A solução foi construída como uma plataforma white-label e multi-tenant, com isolamento lógico entre tenants e Arquitetura Hexagonal para manter o domínio separado das integrações específicas. Trabalhei com Node.js, TypeScript, MySQL, serviços assíncronos em Go e AWS. Estruturei testes unitários e de integração para os casos críticos e usei análise estática como parte do fluxo de qualidade. ### O que levo Essa experiência mudou minha forma de comunicar arquitetura. Passei a tratar alinhamento com produto, PO e stakeholders como parte da decisão técnica, não como uma etapa posterior ao código. --- # Sustentec | Rafael Pereira Source: experiences/sustentec-en.md > My experience at Sustentec with research systems, APIs, and quality. ### Context At Sustentec, I worked on systems connected to laboratories, research, and development. The work combined maintaining an existing product with evolving its features and integrations. ### How I worked I developed a REST API in Dart with Shelf to integrate research-institution databases. I also maintained and evolved a laboratory management system with Java, Spring Boot, JPA, Hibernate, PostgreSQL, and Angular. I added integration tests where that coverage did not exist, developed reports and end-to-end features, and took part in gathering requirements with clients and refining sprints with the Product Owner. ### What I took from it This experience reinforced that quality is not limited to unit tests. In a system with several layers, I needed to validate the actual behavior across API, persistence, and interface. --- # Sustentec | Rafael Pereira Source: experiences/sustentec-es.md > Mi experiencia en Sustentec con sistemas de investigación, APIs y calidad. ### Contexto En Sustentec, trabajé en sistemas relacionados con laboratorios, investigación y desarrollo. La experiencia combinó el mantenimiento de un producto existente con la evolución de funcionalidades e integraciones. ### Cómo trabajé Desarrollé una API REST en Dart con Shelf para integrar bases de instituciones de investigación. También mantuve y evolucioné un sistema de gestión de laboratorios con Java, Spring Boot, JPA, Hibernate, PostgreSQL y Angular. Implementé pruebas de integración donde antes no existía esa cobertura, desarrollé informes y funcionalidades de extremo a extremo y participé en la recopilación de requisitos con clientes y el refinamiento de sprints con el Product Owner. ### Lo que me llevé Esta experiencia reforzó que la calidad no se limita a pruebas unitarias. En un sistema con varias capas, necesitaba validar el comportamiento real entre API, persistencia e interfaz. --- # Sustentec | Rafael Pereira Source: experiences/sustentec-pt-br.md > Minha experiência na Sustentec com sistemas de pesquisa, APIs e qualidade. ### O contexto Na Sustentec, trabalhei em sistemas ligados a laboratórios, pesquisa e desenvolvimento. A experiência combinou a manutenção de um produto existente com a evolução de funcionalidades e integrações. ### Como atuei Desenvolvi uma API REST em Dart com Shelf para integrar bases de instituições de pesquisa. Também mantive e evoluí um sistema de gestão de laboratórios com Java, Spring Boot, JPA, Hibernate, PostgreSQL e Angular. Implementei testes de integração onde antes não havia essa cobertura, desenvolvi relatórios e entregas ponta a ponta e participei da coleta de requisitos com clientes e do refinamento de sprints com o Product Owner. ### O que levo Essa experiência reforçou que qualidade não se limita a testes unitários. Em sistemas com várias camadas, eu precisava validar o comportamento real entre API, persistência e interface. --- # VBET | Rafael Pereira Source: experiences/vbet-en.md > My experience at VBET with security, analytics, and performance at scale. ### Context At VBET, I worked on an analytics product for iGaming affiliates and influencers. It calculated financial and operational metrics in a platform that began serving audiences much larger than originally expected. ### How I worked Before addressing performance, I reduced security and maintenance risks in the legacy API. I replaced unsafe queries, organized the codebase around Clean Architecture and dependency injection, and established tests and documentation to support the following changes. I then addressed performance in stages. I used Go, goroutines, channels, and parallel queries to reduce the first commission-calculation stage from about seven to three minutes. Since the SQL Server was external and could not be changed, I designed an ETL with checkpoints, pre-calculated aggregates, and reconciliation, separating provisional data from consolidated data. ### What I took from it This experience consolidated how I make performance decisions: understand the actual constraint, accept the consistency appropriate to each use, and then choose the technology that addresses it. --- # VBET | Rafael Pereira Source: experiences/vbet-es.md > Mi experiencia en VBET con seguridad, analítica y rendimiento a escala. ### Contexto En VBET, trabajé en un producto de analítica para afiliados e influencers de iGaming. El sistema calculaba métricas financieras y operativas en una plataforma que empezó a atender audiencias mucho mayores de lo previsto originalmente. ### Cómo trabajé Antes de abordar rendimiento, reduje riesgos de seguridad y mantenimiento en la API legada. Reemplacé consultas inseguras, organicé la base con Clean Architecture e inyección de dependencias y establecí pruebas y documentación para sostener los cambios siguientes. Después traté el rendimiento por etapas. Usé Go, goroutines, channels y consultas paralelas para reducir la primera etapa del cálculo de comisiones de cerca de siete a tres minutos. Como el SQL Server era externo y no podía cambiarse, diseñé un ETL con checkpoints, agregaciones precalculadas y reconciliación, diferenciando datos provisionales de datos consolidados. ### Lo que me llevé Esta experiencia consolidó cómo tomo decisiones de rendimiento: entender la restricción real, aceptar la consistencia adecuada para cada uso y después elegir la tecnología que la resuelve. --- # VBET | Rafael Pereira Source: experiences/vbet-pt-br.md > Minha experiência na VBET com segurança, analytics e performance em escala. ### O contexto Na VBET, trabalhei em um produto de analytics para afiliados e influenciadores de iGaming. O sistema calculava métricas financeiras e operacionais em uma plataforma que passou a atender bases de usuários muito maiores do que as previstas originalmente. ### Como atuei Antes de atacar desempenho, comecei reduzindo riscos de segurança e manutenção na API legada. Substituí consultas inseguras, organizei a base com Clean Architecture e injeção de dependências e estabeleci testes e documentação para sustentar as próximas mudanças. Depois, tratei a performance em etapas. Usei Go, goroutines, channels e consultas paralelas para reduzir a primeira etapa do cálculo de comissões de cerca de sete para três minutos. Como o SQL Server era externo e não podia ser alterado, desenhei um ETL com checkpoints, agregações pré-calculadas e reconciliação, distinguindo dados provisórios de dados consolidados. ### O que levo Essa experiência consolidou a forma como tomo decisões de performance: entender o limite real, aceitar a consistência compatível com cada uso e só então escolher a tecnologia que resolve a restrição. --- # Rafael Pereira, senior software engineer Source: pages/home-en.md > Institutional portfolio and editorial hub for Rafael Pereira. I work at the points where simple systems stop being simple. --- # Rafael Pereira, ingeniero de software sénior Source: pages/home-es.md > Portafolio institucional y hub editorial de Rafael Pereira. Trabajo en los puntos en los que los sistemas simples dejan de ser simples. --- # Rafael Pereira, engenheiro de software sênior Source: pages/home-pt-br.md > Portfólio institucional e hub editorial de Rafael Pereira. Trabalho nos pontos em que sistemas simples deixam de ser simples. --- # Verificação da fundação Source: pages/probe-pt-br.md > Página temporária, sem índice, usada para validar a troca de idioma quando não há variante publicada. Esta rota existe só para verificar a fundação multilíngue. Não faz parte da navegação pública nem do sitemap. --- # DiffVision Source: projects/diffvision-en.md > Local-first npm CLI for reviewing Git diffs, with local UI, in-repo comments, Markdown/JSON export, and MCP server. Visual AI review remains a mock. ### Problem Reviewing a Git diff in a SaaS tool sends code away and mixes remote UI with local history. Review needs to work offline, with hunks, filters, bookmarks, and line-anchored comments. ### Constraints Preferences and reports must live in the repository itself (`.diffvision/`). The npm CLI starts local backend and UI. Assistant integration cannot be sold as ready while it is still a prototype. ### Decision CLI inspects Git, parses unified diff, and serves a web interface. Fastify backend with snapshot and WebSocket; React/Vite UI. Markdown/JSON export in the repository. `diffvision-mcp` package over stdio to summarize the repository, read patches, and record comments. The visual AI review assistant is declared mock/prototype; comment writing over MCP works. ### Current state Distributed as an npm CLI, running local-first. ### Limitations It does not replace the GitHub review flow. The visual AI flow must not be read as a finished product. --- # DiffVision Source: projects/diffvision-es.md > CLI npm local-first para revisar diffs Git, con UI local, comentarios en el repositorio, exportación en Markdown/JSON y servidor MCP. La revisión visual por IA permanece mock. ### Problema Revisar un diff Git en herramienta SaaS envía código fuera y mezcla UI remota con el historial local. La revisión necesita funcionar offline, con hunks, filtros, bookmarks y comentarios anclados en líneas. ### Restricciones Preferencias e informes deben vivir en el propio repositorio (`.diffvision/`). La CLI npm inicia backend y UI locales. La integración con asistente no puede venderse como lista si aún es prototipo. ### Decisión La CLI inspecciona el Git, interpreta diff unificado y sube interfaz web. Backend Fastify con snapshot y WebSocket; UI React/Vite. Exportación Markdown/JSON en el repositorio. Paquete `diffvision-mcp` por stdio para resumir el repositorio, leer patches y registrar comentarios. El asistente visual de revisión por IA se declara mock/prototipo; la escritura de comentarios vía MCP es funcional. ### Estado actual Distribuido como CLI npm, con ejecución local-first. ### Limitaciones No sustituye el flujo de review de GitHub. El flujo de IA visual no debe leerse como producto acabado. --- # DiffVision Source: projects/diffvision-pt-br.md > CLI npm local-first para revisar diffs Git, com UI local, comentários no repositório, exportação em Markdown/JSON e servidor MCP. A revisão visual por IA permanece mock. ### Problema Revisar um diff Git em ferramenta SaaS envia código para fora e mistura UI remota com o histórico local. A revisão precisa funcionar offline, com hunks, filtros, bookmarks e comentários ancorados em linhas. ### Restrições Preferências e relatórios devem viver no próprio repositório (`.diffvision/`). A CLI npm inicia backend e UI locais. Integração com assistente não pode ser vendida como pronta se ainda for protótipo. ### Decisão CLI inspeciona o Git, interpreta diff unificado e sobe interface web. Backend Fastify com snapshot e WebSocket; UI React/Vite. Exportação Markdown/JSON no repositório. Pacote `diffvision-mcp` por stdio para resumir repositório, ler patches e registrar comentários. O assistente visual de revisão por IA é declarado mock/protótipo; a escrita de comentários via MCP é funcional. ### Estado atual Distribuído como CLI npm, com execução local-first. ### Limitações Não substitui o fluxo de review do GitHub. O fluxo de IA visual não deve ser lido como produto acabado. --- # goc_mcp Source: projects/goc-mcp-en.md > Local agent orchestration over MCP in Go: maestro, FIFO daemon, OpenCode workers, and SQLite. Delivery worked, but the cost and time hypothesis was not confirmed in this measurement. ### Problem A maestro (Codex or Cursor) needs to delegate work to OpenCode workers with an explicit lifecycle: plan, start, follow, respond, cancel, and recover, without infinite agent recursion. ### Constraints Everything is local. The daemon authenticates on loopback. There are global and per-workspace limits. Interrupted tasks must be reconciled. State corruption must not become blind writes. On the happy path: one OpenCode worker at a time, without automatic decomposition that leaves the maestro without control. ### Decision Go implementation: MCP gateways over stdio, single daemon, state machine, and FIFO executor. SQLite persistence with WAL; JSONL artifacts. OpenCode adapter with server as the primary path and CLI as fallback. Isolation against recursive delegation and read-only diagnostics if state corrupts. ### Current state Repository with tests, ADRs, and benchmarks. Orchestration worked. The measurements published in the project itself did not confirm the hypothesis of reducing time and cost. ### Limitations It is an experiment. It does not claim agent-engineering productivity gains in production. The negative result of the hypothesis is part of the artifact. --- # goc_mcp Source: projects/goc-mcp-es.md > Orquestación local de agentes vía MCP en Go: maestro, daemon FIFO, workers OpenCode y SQLite. La entrega funcionó, pero la hipótesis de costo y tiempo no se confirmó en esta medición. ### Problema Un maestro (Codex o Cursor) necesita delegar trabajo a workers OpenCode con ciclo de vida explícito: planear, iniciar, acompañar, responder, cancelar y recuperar, sin recursión infinita de agentes. ### Restricciones Todo es local. El daemon autentica en loopback. Hay límite global y por workspace. Las tareas interrumpidas necesitan reconciliarse. La corrupción de estado no puede volverse escritura ciega. En el camino feliz: un worker OpenCode por vez, sin descomposición automática que deje al maestro sin control. ### Decisión Implementación en Go: gateways MCP por stdio, daemon único, máquina de estados y ejecutor FIFO. Persistencia en SQLite con WAL; artefactos en JSONL. Adaptador OpenCode con servidor como camino principal y CLI como fallback. Aislamiento contra delegación recursiva y diagnóstico solo lectura si el estado se corrompe. ### Estado actual Repositorio con pruebas, ADRs y benchmarks. La orquestación funcionó. Las mediciones publicadas en el propio proyecto no confirmaron la hipótesis de reducir tiempo y costo. ### Limitaciones Es un experimento. No afirma ganancia de productividad de ingeniería de agentes en producción. El resultado negativo de la hipótesis forma parte del artefacto. --- # goc_mcp Source: projects/goc-mcp-pt-br.md > Orquestração local de agentes via MCP em Go: maestro, daemon FIFO, workers OpenCode e SQLite. A entrega funcionou, mas a hipótese de custo e tempo não foi confirmada nesta medição. ### Problema Um maestro (Codex ou Cursor) precisa delegar trabalho a workers OpenCode com ciclo de vida explícito: planejar, iniciar, acompanhar, responder, cancelar e recuperar, sem recursão infinita de agentes. ### Restrições Tudo é local. O daemon autentica em loopback. Há limite global e por workspace. Tarefas interrompidas precisam ser reconciliadas. Corrupção de estado não pode virar escrita cega. No caminho feliz: um worker OpenCode por vez, sem decomposição automática que deixe o maestro sem controle. ### Decisão Implementação em Go: gateways MCP por stdio, daemon único, máquina de estados e executor FIFO. Persistência em SQLite com WAL; artefatos em JSONL. Adaptador OpenCode com servidor como caminho principal e CLI como fallback. Isolamento contra delegação recursiva e diagnóstico somente leitura se o estado corromper. ### Estado atual Repositório com testes, ADRs e benchmarks. A orquestração funcionou. As medições publicadas no próprio projeto não confirmaram a hipótese de reduzir tempo e custo. ### Limitações É um experimento. Não afirma ganho de produtividade de engenharia de agentes em produção. O resultado negativo da hipótese faz parte do artefato. --- # md2cv Source: projects/md2cv-en.md > Local-first desktop studio for professional profiles, Markdown resumes, immutable versions, ATS, and job matching with supervised agents. Data stays in the machine's SQLite. ### Problem Professional profiles scatter across docs, LinkedIn, and exports. Each application asks for a different angle. Adapting a resume with an unbounded LLM invents experience and erases the context of the previous version. What is needed is a versioned graph on the machine that asks what is missing and rejects incomplete output, without promising hiring or automatic ATS approval. ### Constraints Desktop and local-first (Electron): no proprietary account and no backend owning the data. - Typed and validated IPC between renderer and main process. - SQLite with foreign keys, WAL, checksummed migrations, integrity checks, and backup before destructive operations. - Agents (Codex, Cursor, OpenCode) enter through isolated adapters, not as database owners. - Job matching never writes to the profile without explicit confirmation. - External access only on user action (company URL lookup or an AI CLI already configured on the computer); the product does not store provider credentials. ### Decision React/Vite renderer separated from the main process. The product organizes work in a single local graph: - Profile: experiences, education, courses, languages, projects, links, skills, and companies per person. - Resumes: Markdown, immutable versions, traceable restore, and evolution overview. - ATS: structural diagnostics and guiding scores; marked textual PDF and semantic DOCX. - Applications: company, job post, base resume, version, and state in the same context. - Agents: state machine for questions, attempts, and proposed new versions under schema; persists only on confirmation. - Data: versioned import and export of the full graph; Markdown compiler (unified/remark) shared by preview, audit, and export. Canonical flow: profile → base resume → immutable version → ATS audit → PDF/DOCX. The application branch goes through a supervised local agent before the adapted resume. ### Current state Public repository under MIT, documentation portal, and x64 releases for Linux (AppImage, `.deb`, `.rpm`) and Windows (NSIS and portable), with CI and Vitest and Playwright tests on persistence, agents, ATS, and Electron flows. The professional export used on this site comes from this product and is not read by the site at runtime. ### Limitations It does not replace human review nor promise hiring or automatic approval by recruiting platforms. Job matching depends on confirmed answers; the system refuses to fabricate experience. Windows binaries in this phase are not code-signed. The project is author-owned with no commercial purpose from the maintainer; the MIT license allows reuse under its terms. --- # md2cv Source: projects/md2cv-es.md > Estudio desktop local-first para perfil profesional, currículos Markdown, versiones inmutables, ATS y adaptación a vacantes con agentes supervisados. Los datos quedan en el SQLite de la máquina. ### Problema Los perfiles profesionales se esparcen entre docs, LinkedIn y exports. Cada candidatura pide un ángulo distinto. Adaptar el currículo con un LLM sin frontera inventa experiencia y borra el contexto de la versión anterior. Se necesita un grafo versionado en la máquina que pregunte lo que falta y rechace salida incompleta, sin prometer contratación ni aprobación automática por ATS. ### Restricciones Desktop y local-first (Electron): sin cuenta propietaria y sin backend dueño de los datos. - IPC tipado y validado entre renderer y proceso principal. - SQLite con foreign keys, WAL, migraciones con checksum, verificaciones de integridad y backup antes de operación destructiva. - Agentes (Codex, Cursor, OpenCode) entran por adaptadores aislados, no como dueños de la base. - La adaptación a una candidatura no graba en el perfil sin confirmación explícita. - Acceso externo solo bajo acción del usuario (búsqueda de URL de empresa o CLI de IA ya configurada en el equipo); el producto no almacena credenciales de proveedores. ### Decisión Renderer React/Vite separado del proceso principal. El producto organiza el trabajo en un único grafo local: - Perfil: experiencias, formación, cursos, idiomas, proyectos, enlaces, habilidades y empresas por persona. - Currículos: Markdown, versiones inmutables, restauración rastreable y panorama de evolución. - ATS: diagnósticos estructurales y puntuación orientativa; PDF textual marcado y DOCX semántico. - Candidaturas: empresa, vacante, currículo base, versión y estado en el mismo contexto. - Agentes: máquina de estados para preguntas, intentos y propuesta de nueva versión bajo schema; solo persiste con confirmación. - Datos: importación y exportación versionada del grafo completo; compilador Markdown (unified/remark) compartido por preview, auditoría y export. Flujo canónico: perfil → currículo base → versión inmutable → auditoría ATS → PDF/DOCX. La rama de candidatura pasa por agente local supervisado antes del currículo adaptado. ### Estado actual Repositorio público bajo MIT, portal de documentación y releases x64 para Linux (AppImage, `.deb`, `.rpm`) y Windows (NSIS y portable), con CI y pruebas Vitest y Playwright en persistencia, agentes, ATS y flujos Electron. El export profesional usado en este sitio nace de ese producto y no es leído por el sitio en runtime. ### Limitaciones No sustituye la revisión humana ni promete contratación o aprobación automática por plataformas de reclutamiento. La adaptación a vacantes depende de respuestas confirmadas; el sistema se niega a fabricar experiencia. Los binarios Windows de esta fase no poseen firma de código. El proyecto es autoral y sin finalidad comercial del mantenedor; la licencia MIT permite reutilización en sus términos. --- # md2cv Source: projects/md2cv-pt-br.md > Estúdio desktop local-first para perfil profissional, currículos Markdown, versões imutáveis, ATS e adaptação a vagas com agentes supervisionados. Os dados ficam no SQLite da máquina. ### Problema Perfis profissionais espalham-se entre docs, LinkedIn e exports. Cada candidatura pede um ângulo diferente. Adaptar o currículo com um LLM sem fronteira inventa experiência e apaga o contexto da versão anterior. É preciso um grafo versionado na máquina que pergunte o que falta e rejeite saída incompleta, sem prometer contratação ou aprovação automática por ATS. ### Restrições Desktop e local-first (Electron): sem conta proprietária e sem backend dono dos dados. - IPC tipado e validado entre renderer e processo principal. - SQLite com foreign keys, WAL, migrations com checksum, verificações de integridade e backup antes de operação destrutiva. - Agentes (Codex, Cursor, OpenCode) entram por adaptadores isolados, não como donos do banco. - Adaptação a candidatura não grava no perfil sem confirmação explícita. - Acesso externo só sob ação do usuário (pesquisa de URL de empresa ou CLI de IA já configurada no computador); o produto não armazena credenciais de provedores. ### Decisão Renderer React/Vite separado do processo principal. O produto organiza o trabalho em um único grafo local: - Perfil: experiências, formação, cursos, idiomas, projetos, links, habilidades e empresas por pessoa. - Currículos: Markdown, versões imutáveis, restauração rastreável e panorama de evolução. - ATS: diagnósticos estruturais e pontuação orientativa; PDF textual marcado e DOCX semântico. - Candidaturas: empresa, vaga, currículo base, versão e estado no mesmo contexto. - Agentes: máquina de estados para perguntas, tentativas e proposta de nova versão sob schema; só persiste com confirmação. - Dados: importação e exportação versionada do grafo completo; compilador Markdown (unified/remark) compartilhado por preview, auditoria e export. Fluxo canônico: perfil → currículo base → versão imutável → auditoria ATS → PDF/DOCX. O ramo de candidatura passa por agente local supervisionado antes do currículo adaptado. ### Estado atual Repositório público sob MIT, portal de documentação e releases x64 para Linux (AppImage, `.deb`, `.rpm`) e Windows (NSIS e portátil), com CI e testes Vitest e Playwright em persistência, agentes, ATS e fluxos Electron. O export profissional usado neste site nasce desse produto e não é lido pelo site em runtime. ### Limitações Não substitui revisão humana nem promete contratação ou aprovação automática por plataformas de recrutamento. A adaptação a vagas depende de respostas confirmadas; o sistema recusa fabricar experiência. Binários Windows desta fase não possuem assinatura de código. O projeto é autoral e sem finalidade comercial do mantenedor; a licença MIT permite reutilização nos seus termos. --- # Post Engine Source: projects/post-engine-en.md > Authorship-centered editorial workstation: interview, briefing, storyboard, and export after the gateway blocks fabricated content. Versioned prompts; isolated LLM workspace. ### Problem Generating a "professional" post with an LLM from a slogan invents biography. The flow must interview, identify gaps, prepare the briefing, draft, and export — and refuse what was not confirmed. ### Constraints Python core with boundaries between interview, generation, authorship preservation, segmentation, and persistence. Model calls in an isolated workspace, provider allowlist, and prompts as versioned contracts. Hybrid evaluation: LLM plus deterministic heuristics. Missing experience never becomes false lived experience. ### Decision Adaptive interviews, authorial briefing, storyboard, veto on fabricated content, SQLite prompt registry with rollback, textual interface, and React/Vite frontend to review phases. Markdown or SlideMark JSON export after evaluation. ### Current state Base with tests for interview, LLM isolation, registry, persistence, and SlideMark conversion. ### Limitations It is not a generic thought-leadership generator. Without real repertoire, the system refuses; it does not complete the biography. --- # Post Engine Source: projects/post-engine-es.md > Estación editorial centrada en autoría: entrevista, briefing, storyboard y exportación después de que el gateway bloquee contenido fabricado. Prompts versionados; workspace LLM aislado. ### Problema Generar un post "profesional" con LLM a partir de un eslogan inventa biografía. El flujo necesita entrevistar, identificar lagunas, preparar el briefing, redactar y exportar, y rechazar lo no confirmado. ### Restricciones Núcleo en Python con fronteras entre entrevista, generación, preservación de autoría, segmentación y persistencia. Llamadas a modelo en workspace aislado, allowlist de proveedor y prompts como contratos versionados. Evaluación híbrida: LLM más heurísticas deterministas. La ausencia de experiencia nunca se vuelve falsa vivencia. ### Decisión Entrevistas adaptativas, briefing autoral, storyboard, veto a contenido fabricado, registry SQLite de prompts con rollback, interfaz textual y frontend React/Vite para revisar fases. Exportación Markdown o SlideMark JSON tras la evaluación. ### Estado actual Base con pruebas de entrevista, aislamiento de LLM, registry, persistencia y conversión SlideMark. ### Limitaciones No es un generador genérico de thought leadership. Sin repertorio real, el sistema se niega; no completa la biografía. --- # Post Engine Source: projects/post-engine-pt-br.md > Workstation editorial centrada em autoria: entrevista, briefing, storyboard e exportação após o gateway barrar conteúdo fabricado. Prompts versionados; workspace LLM isolado. ### Problema Gerar post “profissional” com LLM a partir de um slogan inventa biografia. O fluxo precisa entrevistar, identificar lacunas, preparar o briefing, redigir e exportar, e recusar o que não foi confirmado. ### Restrições Núcleo em Python com fronteiras entre entrevista, geração, preservação de autoria, segmentação e persistência. Chamadas a modelo em workspace isolado, allowlist de provedor e prompts como contratos versionados. Avaliação híbrida: LLM mais heurísticas determinísticas. Ausência de experiência nunca vira falsa vivência. ### Decisão Entrevistas adaptativas, briefing autoral, storyboard, veto a conteúdo fabricado, registry SQLite de prompts com rollback, interface textual e frontend React/Vite para revisar fases. Exportação Markdown ou SlideMark JSON após avaliação. ### Estado atual Base com testes de entrevista, isolamento de LLM, registry, persistência e conversão SlideMark. ### Limitações Não é um gerador genérico de thought leadership. Sem repertório real, o sistema recusa; não completa a biografia. --- # SMS Manager Source: projects/sms-manager-en.md > Decoupled SMS campaigns: the Nest API persists and publishes, the queue delivers, and consumers in TypeScript, Go, or Rust record the result. Opaque token, Redis, and gRPC between services. ### Problem SMS campaigns start from CSV files, users, companies, and authentication between services. Validating and persisting in the same process that fires thousands of messages couples the API to the pace of the carrier and the queue. ### Constraints The environment must be reproducible. Authentication between services cannot depend on JWT without revocation. Consumers in more than one language exist to compare the same messaging contract. ### Decision Seven applications: NestJS user/auth and company/campaign APIs; equivalent consumers in Node.js/TypeScript, Go, and Rust; declarative provisioner for exchanges, queues, and bindings; CSV bulk generator. PostgreSQL/TypeORM for relational API data; MongoDB for consumption results; Redis for opaque revocable token cache; gRPC for authentication between services; RabbitMQ with topic exchanges for batches. The API publishes and moves on without blocking on the carrier pace. ### Current state Public repository with Docker Compose for PostgreSQL, MongoDB, Redis, and RabbitMQ. The architecture separates domain, application, and infrastructure in the users, authentication, and companies contexts; the three consumers implement the same message contract. ### Limitations It is a study artifact and local messaging operation, not a commercial product with a carrier SLA. The three consumers demonstrate the contract; they do not claim the three languages run together in the author's production. --- # SMS Manager Source: projects/sms-manager-es.md > Campañas de SMS desacopladas: la API Nest persiste y publica, la cola entrega y consumidores en TypeScript, Go o Rust graban el resultado. Token opaco, Redis y gRPC entre servicios. ### Problema Las campañas de SMS parten de archivos CSV, usuarios, empresas y autenticación entre servicios. Validar y persistir en el mismo proceso que dispara miles de mensajes acopla la API al ritmo de la operadora y de la cola. ### Restricciones El entorno debe ser reproducible. La autenticación entre servicios no puede depender de JWT opaco sin revocación. Los consumidores en más de un lenguaje existen para comparar el mismo contrato de mensajería. ### Decisión Siete aplicaciones: APIs NestJS de usuarios/autenticación y de empresas/campañas; consumidores equivalentes en Node.js/TypeScript, Go y Rust; aprovisionador declarativo de exchanges, colas y bindings; generador de masa CSV. PostgreSQL/TypeORM para datos relacionales en la API; MongoDB para el resultado del consumo; Redis para caché de token opaco (revocable); gRPC para autenticación entre servicios; RabbitMQ con exchanges topic para los lotes. La API publica y sigue sin bloquearse al ritmo de la operadora. ### Estado actual Repositorio público con Docker Compose para PostgreSQL, MongoDB, Redis y RabbitMQ. La arquitectura separa dominio, aplicación e infraestructura en los contextos de usuarios, autenticación y empresas; los tres consumidores implementan el mismo contrato de mensaje. ### Limitaciones Es un artefacto de estudio y operación local de mensajería, no un producto comercial con SLA de operadora. Los tres consumidores demuestran el contrato; no afirman que los tres lenguajes corran juntos en producción del autor. --- # SMS Manager Source: projects/sms-manager-pt-br.md > Campanhas de SMS desacopladas: a API Nest persiste e publica, a fila entrega e consumidores em TypeScript, Go ou Rust gravam o resultado. Token opaco, Redis e gRPC entre serviços. ### Problema Campanhas de SMS partem de arquivos CSV, usuários, empresas e autenticação entre serviços. Validar e persistir no mesmo processo que dispara milhares de mensagens acopla a API ao ritmo da operadora e da fila. ### Restrições O ambiente precisa ser reproduzível. Autenticação entre serviços não pode depender de JWT opaco sem revogação. Consumidores em mais de uma linguagem existem para comparar o mesmo contrato de mensageria. ### Decisão Sete aplicações: APIs NestJS de usuários/autenticação e de empresas/campanhas; consumidores equivalentes em Node.js/TypeScript, Go e Rust; provisionador declarativo de exchanges, filas e bindings; gerador de massa CSV. PostgreSQL/TypeORM para dados relacionais na API; MongoDB para resultado do consumo; Redis para cache de token opaco (revogável); gRPC para autenticação entre serviços; RabbitMQ com exchanges topic para os lotes. A API publica e segue sem bloquear no ritmo da operadora. ### Estado atual Repositório público com Docker Compose para PostgreSQL, MongoDB, Redis e RabbitMQ. A arquitetura separa domínio, aplicação e infraestrutura nos contextos de usuários, autenticação e empresas; os três consumidores implementam o mesmo contrato de mensagem. ### Limitações É um artefato de estudo e operação local de mensageria, não um produto comercial com SLA de operadora. Os três consumidores demonstram o contrato; não afirmam que as três linguagens rodam juntas em produção do autor. --- # Currículo | Rafael Pereira Source: resumes/curriculo-pt-br.md > Currículo online de Rafael Pereira, engenheiro backend sênior com experiência em Node.js, Go e Java, e atuação complementar em React e Angular. --- # Currículum | Rafael Pereira Source: resumes/curriculum-es.md > Currículum online de Rafael Pereira, ingeniero backend senior con experiencia en Node.js, Go y Java, y trabajo complementario con React y Angular. --- # Resume | Rafael Pereira Source: resumes/resume-en.md > Rafael Pereira's online resume: a senior backend engineer with experience in Node.js, Go, and Java, plus complementary work with React and Angular. --- # Gabriel | Sports Nutrition Source: visual-projects/gabriel-nutricao-en.md > Institutional site for Gabriel, focused on sports nutrition for people who train. Institutional site for Gabriel Pereira around sports nutrition, with real content and strategy for a training routine. --- # Gabriel | Nutrición Deportiva Source: visual-projects/gabriel-nutricao-es.md > Sitio institucional para Gabriel, enfocado en nutrición deportiva para quienes entrenan. Sitio institucional de Gabriel Pereira sobre nutrición deportiva, con contenido real y estrategia para una rutina de entrenamiento. --- # Gabriel | Nutrição Esportiva Source: visual-projects/gabriel-nutricao-pt-br.md > Site institucional para Gabriel, com foco em nutrição esportiva para quem treina. --- # Gran Goiás Source: visual-projects/gran-goias-en.md > Institutional site for Gran Goiás, with stone execution for large-scale works. Institutional site for Gran Goiás, built around stone execution for large-scale works. --- # Gran Goiás Source: visual-projects/gran-goias-es.md > Sitio institucional para Gran Goiás, con ejecución en piedra para obras de escala. Sitio institucional de Gran Goiás, construido en torno a la ejecución en piedra para obras de escala. --- # Gran Goiás Source: visual-projects/gran-goias-pt-br.md > Site institucional para a Gran Goiás, com execução em pedra para obras de escala. --- ## About This Document This concatenated documentation file is generated automatically by aeo.js to make it easier for AI systems to understand the complete context of this project. For a structured index, see: https://imrafaeldev.site/llms.txt For individual files, see: https://imrafaeldev.site/docs.json Generated by aeo.js - https://aeojs.org