Quando as pessoas perguntam sobre a pilha de tecnologia do Nubank, a resposta é bem curta. Usamos as mesmas poucas tecnologias para a maioria dos nossos sistemas de back-end: Clojure, para serviços de produção, Kafka, para comunicação assíncrona, Datomic, como base de dados para dados de negócios de alto valor, Scala, para nosso ambiente analítico e Flutter, para nosso aplicativo móvel.

Após sete anos desenvolvendo produtos, agora com mais de 600 engenheiros, alguém pode nos perguntar como nos firmamos com essas tecnologias específicas. Contudo, a pergunta que queremos responder aqui é:

O que acontece quando restringimos a quantidade de ferramentas de uma empresa de engenharia “para o bem maior”?

Mais especificamente, se preferimos ter menos variedade em nossa escolha e nosso uso de tecnologias, favorecendo abordagens canônicas, isso resultaria em uma empresa de engenharia mais eficiente?

O que é variedade de tecnologia? É tão ruim assim?

“Ter menos variedade” pode ser lido mais precisamente como “evitar variedades não essenciais”. Portanto, queremos incentivar mudanças quando uma nova situação demanda o uso de tecnologias diferentes, que fornecem um valor melhor para determinada tarefa, ou seja, variedade essencial. Por outro lado, não queremos escolher uma nova tecnologia apenas pelo desejo de usar uma diferente, ou seja, variedade não essencial. Em outras palavras, preferimos ter formas canônicas de tratar as coisas.

Quando o assunto é “poucas tecnologias”, alguns engenheiros poderiam se deparar com algo do tipo: “você concordaria em continuar usando uma linguagem antiga, como COBOL, pelo resto da sua vida?” Há um argumento válido aqui: em várias situações, limitar as opções pode parecer, bom, limitante, principalmente quando os benefícios são de longo prazo e diluídos pela empresa.

Mas a preferência por menos variedade não é o mesmo que evitar a evolução ou ignorar alternativas. Isso significa que na troca do “COBOL pelo resto da sua vida” para “tecnologia novíssima em folha toda semana”, nossa tendência é ir para o lado de solucionar problemas consistentemente em toda a empresa (nessa analogia, mais para o lado do COBOL provavelmente).

“Escolher o lado do COBOL” é uma frase curiosa em 2020, mas desvendar o que isso significa na prática é crucial. Queremos usar uma ferramenta excelente para cada trabalho; se o que temos agora atende os requisitos e as alternativas não apresentam um benefício claro, preferimos não adicionar novas tecnologias.

Se a situação for diferente, não temos medo de aprimorar nossa caixa de ferramentas e começar a usar uma nova. Por exemplo, quando desenvolvemos a primeira versão dos nossos trabalhos de ETL, decidimos usar Spark com Scala em vez de Clojure. Ou quando começamos a desenvolver serviços envolvendo Aprendizado de Máquina, escolhemos Python, outra vez no lugar de Clojure.

Nós as vimos como ferramentas muito melhores para essas situações no contexto da época. Foram decisões estratégicas e ponderadas. É isso o que buscamos cada vez que pensamos em nos desviar no nosso uso de tecnologias.

Conheça nossas oportunidades

Variação Interna

Outro aspecto relevante é que a variação não está presente apenas quando escolhemos uma tecnologia nova, por exemplo, uma nova base de dados ou estrutura. Isso também se relaciona com o uso das tecnologias que já temos. Ao escolher uma linguagem de programação, por exemplo, podemos ver o estilo do código, características, estruturas e bibliotecas como fontes de variação. Várias delas são tão flexíveis que é possível fazer com que dois trechos de códigos escritos com a mesma tecnologia sejam estranhos um para o outro. A maioria dessas diferenças ocorre de forma orgânica, principalmente com uma quantidade crescente de engenheiros, como é natural que as opiniões sejam diferentes quanto ao uso de uma tecnologia. Por isso, evitar variações não essenciais se faz necessário não apenas em um nível mais alto (como linguagens de programação e base de dados), mas também em um nível interno (como estilos de código e características de linguagem) quando nos esforçamos para manter a canonicidade no nível organizacional.

Quando avaliamos nosso uso de tecnologias no Nubank, vemos um alto nível de consistência em como as usamos.

Por exemplo, se selecionarmos dois códigos-base do Clojure para produção aleatória, a estrutura do código (arquivos e pastas) seria muito semelhante para ambos. Eles usariam as mesmas bibliotecas e estruturas provavelmente. E, por fim, há a própria linguagem do Clojure. Ele é flexível, e os programadores podem usá-lo de diferentes formas (olá, macros!), mas também é simples e incentiva abordagens canônicas para problemas comuns. Ao observar esses dois serviços aleatórios, você provavelmente pensaria que a mesma equipe escreveu os dois códigos.

Não é fácil alcançar ou manter a padronização do uso; é preciso ter intenção, revisão de código difundido, supervisão sênior e automação (como modelos para novos serviços). Esses são apenas alguns exemplos.

Embora toda essa consistência e homogeneidade na escolha e uso de tecnologias pareça convincente, os verdadeiros benefícios podem não estar explícitos. Por que se importar com isso?

Mitigação de dependência

O Nubank se divide em grupos de pequenas equipes multifuncionais. Queremos que elas sejam autossuficientes ao desenvolver novas coisas com o mínimo de dependência das outras. Uma equipe deve ser capaz de codificar, testar e implementar algo para a produção sem esperar (depender) que outra equipe faça algum trabalho como implementar recursos, executar pipelines, criar novas infraestruturas etc. Conseguimos eliminar várias dependências com a automatização incessante, mas com uma quantidade de sistemas em constante crescimento, é inevitável que as equipes comecem a se especializar e deter pequenas partes do código-base. Nesse contexto, em algum momento, os engenheiros enfrentarão tarefas que exigem trabalhar fora do domínio de suas equipes e códigos-base. Uma situação típica é quando um grupo precisa acessar dados em um serviço de outra equipe (dados que ainda não tenham sido expostos por uma API pré-existente). Um possível resultado seria os engenheiros dependerem, ou seja, aguardarem, que a outra equipe crie um novo ponto final para o acesso aos dados.

Conseguimos mitigar bastante esse tipo de dependência usando um princípio simples: todos os softwares no Nubank devem estar abertos para colaboração. Isso significa que todo engenheiro deve ter permissão e ser capaz de propor uma alteração em qualquer serviço. Obviamente, o ideal é expor a proposta e combinar os detalhes com os proprietários do serviço. As contribuições serão revisadas, aprovadas e mescladas por eles, que serão responsáveis por essa operação confiável. Na prática, isso funciona melhor do que esperar que os proprietários mudem suas prioridades para acomodar as demandas de uma equipe separada. Portanto, qualquer engenheiro tem a habilidade de criar o novo ponto final por conta própria.

Isso é ótimo na teoria, mas será que é fácil codificar em um código-base estranho que você conheceu há cinco minutos? Ele pode estar em uma linguagem de programação diferente; uma que você não costuma usar. Ou talvez use uma nova base de dados NoSQL que foi lançada no ano passado, sobre a qual você apenas leu no Hacker News. Ele pode até usar as mesmas tecnologias que você conhece, mas de uma forma totalmente diferente, como um código com características OO em vez de funcional. Qualquer uma dessas possibilidades pode criar barreiras tecnológicas para a colaboração.

Quando isso acontece, ter menos itens em nossa caixa de ferramentas ajuda bastante. No Nubank, há uma chance muito alta de que a sua equipe e uma equipe distinta usem Clojure para serviços de back-end. E de ambas usarem Kafka. E de ambas usarem Datomic. E de ambas terem estilos de códigos semelhantes.

Portanto, você pode focar em entender o domínio, o problema dos negócios a ser solucionado, o status quo do código-base e como ele precisa evoluir.

Essa facilidade para alterar serviços alheios pode causar problemas (geralmente, semelhantes ao código aberto, quando as contribuições não estão seguindo na direção desejada de evolução do código-base), mas a revisão do código e o excesso de comunicação são proteções eficientes contra eles. Afinal, não estamos eliminando a dependência entre as equipes completamente; estamos apenas otimizando a resolução prática de um potencial bloqueador. Estamos mantendo nossas dependências explícitas entre os serviços, em vez de deixá-las entre cartas em backlogs diferentes. Nós alteramos essa dependência em busca de processos mais leves: alinhamento e revisão de códigos.

No Nubank, essa natureza de código colaborativa tem sido essencial para que as equipes fiquem em seu fluxo o máximo possível. Isso também ajudou a criar uma mentalidade de propriedade de código em toda a empresa, a qual tem sido fácil evoluir com a nossa estrutura organizacional (mitigando ligeiramente a lei de Conway). Tudo isso foi possível pelo fato de não termos barreiras tecnológicas entre as equipes.

Transferências entre as equipes são menos complicadas

No megacrescimento que o Nubank vivenciou nos últimos anos, algo ficou evidente: as prioridades mudam. Uma consequência é que nós geralmente temos que formar novas equipes ou alterar as atuais, e isso envolve transferir engenheiros.

Então, o que acontece quando um engenheiro é transferido para outra equipe? Além de se acostumar com novas pessoas e dinâmicas, o principal desafio é aprender um novo domínio técnico e de negócios: O que é o produto? Quem é o cliente? Quais serviços nós temos? Quais tecnologias nós usamos?

Não podemos afirmar que fomos capazes de tornar essa transição irrelevante ou que ela não foi difícil. Mas se um engenheiro de um grupo pode ir rapidamente para o serviço de outra equipe, entender o código e propor mudanças, isso provavelmente significa que ele pode entender o contexto técnico da nova equipe muito mais rápido.

Embora nosso desejo seja evitar confusões ao transferir pessoas entre grupos e contextos com rapidez, ao longo dos anos, os engenheiros foram capazes de se mudar rapidamente, sem muita preocupação em aprender novas tecnologias. Eles podem focar em entender o domínio de negócios específico da nova equipe, e isso não é uma tarefa fácil. A facilidade de transferir engenheiros nos dá mais flexibilidade para colocá-los em prioridades mais altas ou em posições melhores para crescerem em suas carreiras, sem perturbar demais a produtividade.

Melhorias técnicas de alto impacto

Em grande escala, tudo dá problema em algum momento, e queremos consertar cada coisa apenas uma vez, de preferência. No mesmo sentido, até pequenas melhorias na produtividade da engenharia podem ter impactos enormes.

Suponhamos que você tenha uma empresa com cinco engenheiros; todos usam serviços baseados em Java. Se você decide melhorar a vida deles com, digamos, um linter no pipeline de construção, pode ter certeza de que melhorou a produtividade de todos os seus engenheiros. Imagine que, além dos cinco engenheiros da primeira equipe, você tenha outra equipe de cinco engenheiros que usa Clojure em vez de Java. Investir tempo para implementar o linter de Java não trará benefícios para os engenheiros que usam Clojure.

Toda “divisão virtual” (como linguagem de programação, estruturas, base de dados) indica que você está diminuindo o impacto ao melhorar as ferramentas. O uso do que você está alterando limita o raio de impacto da sua melhoria. Se ampliarmos o exemplo anterior para 200 ou 1000 engenheiros, as diferenças ficam ainda mais evidentes. Peter Seibel explica concisamente:

“Quando sua organização de engenheiros atinge um certo tamanho, os benefícios obtidos após você ter investido em aumentar levemente a produtividade dos engenheiros começa a ultrapassar os pequenos ganhos que uma equipe pode ter ao atuar sozinha, de uma forma um pouco diferente.”

Quando observamos equipes horizontais (por exemplo, infraestrutura, produtividade de engenharia, segurança), a situação fica ainda mais aparente. Toda “divisão virtual” significa que você está colocando mais trabalho nos backlogs deles, pois as melhorias e ajustes para uma tecnologia não passam necessariamente para outra.

Um exemplo real dessa situação é a segurança de serviço. Nós o implementamos para serviços de Clojure, e toda nova melhoria ou ajuste relacionados à segurança podem ser facilmente estendidos para todos os nossos serviços. Por exemplo, se tivéssemos serviços Node.js, primeiro precisaríamos reimplementar toda a lógica de segurança nessa nova plataforma; depois, cada vez que quiséssemos uma melhoria, a equipe de segurança da informação precisaria implementar tanto no código de Clojure (baseado em JVM) quanto no código Node.js.

Outra forma de ver isso é pelas bibliotecas em comum. Trechos de código que costumam ser usados em vários serviços são normalmente extraídos para bibliotecas (ou serviços de plataforma, dependendo do caso). Temos bibliotecas padrão para comunicações com Datomic, DynamoDB, para produzir e consumir do Kafka, para fazer solicitações HTTP para outros serviços, processar arquivos posicionais, gerar PDFs e muito mais. Para cada tempo de execução diferente para os quais temos suporte oficial, aumentamos o esforço necessário para manter e evoluir os padrões comuns.

Construindo a estrada e saindo dela ao mesmo tempo

A normalidade é uma estrada pavimentada: É confortável para caminhar, mas flores não crescem por lá

Vincent van Gogh

Os benefícios podem ser diretos, mas como e quando devemos nos desviar das abordagens canônicas e introduzir variações? Nossa escolha de selecionar menos tecnologias e melhorá-las ativamente é como construir uma “estrada pavimentada”, o que significa fazer uma viagem tranquila e com o mínimo de esforço possível enquanto programamos no Nubank, com ferramentas refinadas e eficientes para os trabalhos mais comuns. Mas ter uma boa estrada pavimentada não quer dizer que ficamos nela o tempo todo. Como Van Gogh disse: “A normalidade é uma estrada pavimentada: É confortável para caminhar, mas flores não crescem por lá.”

Então, enquanto nossa estrada principal deve ser o caminho de menor resistência, acelerando nosso loop interno para problemas comuns, às vezes, precisamos “sair da rota” e procurar as flores. Isso deve acontecer quando uma ferramenta que já usamos: (1) não é ideal e há opções melhores; ou (2) é inútil para o trabalho. Porém, não basta sair da rota. Se continuarmos abrindo novos caminhos, arriscamos nunca ter tempo, energia e poder para pavimentar nossa estrada principal, deixando-a sempre melhor.

Continuando com a metáfora das flores, Peter Seibel abordou o mesmo conceito de forma memorável no título de seu artigo de 2015: “Let a 1,000 flowers bloom. Then rip 999 of them out by the roots” (Deixe 1000 flores florescerem. Depois, arranque 999 delas pelas raízes). Isso quer dizer que as equipes devem ser incentivadas a experimentar com ideias de forma autônoma, ou seja, flores florescendo longe da estrada pavimentada, sabendo que a maioria não terá “sucesso”; elas sabem que serão reprovadas e não serão mantidas ativamente, isto é, serão arrancadas.  Mas quando obtêm sucesso, investimos nelas e as tornamos parte da nossa estrada pavimentada.

Nossas tecnologias móveis são um bom exemplo disso. No começo do Nubank, em 2014, usávamos Java (Android) e Objective-C (iOS). Quando surgiram ferramentas melhores, começamos a usá-las e migramos para Kotlin (Android) e Swift (iOS). Em algum momento, nossa equipe de contas bancárias experimentou o React Native, uma nova tecnologia multiplataforma na época. Embora a maior parte da funcionalidade do aplicativo relacionada ao cartão de crédito tenha permanecido a mesma, as telas de contas bancárias estavam todas no React Native.

A equipe aprendeu muito com isso e, então, decidiu experimentar o Flutter para ver se este apresentaria uma experiência aprimorada para os desenvolvedores e uma melhor solidez na cadeia de ferramentas. Então, alguns recursos foram codificados no Flutter. Algumas flores floresceram e, devido à fragmentação insalubre que estava ocorrendo, tivemos que escolher em qual flor investir. Leia mais sobre isso neste artigo, mas estragando a surpresa, o Flutter agora é a nossa “estrada pavimentada móvel”, embora ainda esteja em construção.

O florescimento e o descarte de flores não são necessariamente decisões únicas ou marcos no tempo. Eles costumam ser um processo que ocorre ao longo de meses, ou até anos. Aqui estão alguns exemplos do Nubank:

  • Temos usado uma estrutura própria de teste de integração desde o início do Nubank, que posteriormente foi recriada em código aberto como Selvage. Nos últimos anos, também experimentamos uma nova estrutura chamada State-flow. Recentemente, escolhemos padronizar esta última e descartar as outras variantes. O valor de negócios de fazer uma migração completa e poderosa é menos evidente, porém esperamos que essa transição continue organicamente por enquanto.
  • Um caso de algo “não funcionar para o trabalho” aconteceu quando tentamos usar ClojureScript com o React Native. Mesmo usando o ClojureScript amplamente na internet, infelizmente, naquela época, havia limitações demais para usar em nosso aplicativo. Decidimos que o ClojureScript não era viável para o aplicativo e optamos por usar Typescript.
  • Nós dependíamos do Riemann para todo o nosso monitoramento, mas acabamos trocando para um ecossistema com mais recursos, o Prometheus, em uma migração completa que levou alguns meses.
  • Quanto aos serviços de back-end para o front-end, adotamos principalmente APIs gráficas, como GraphQL e Pathom, em vez de REST.  Uma decisão clara para padronizar uma delas ou trabalhar de forma mista está em andamento.
  • Para as interfaces de internet, nós usamos re-frame amplamente, e nos últimos anos, temos experimentado o Pathom e oFulcro. Ao mesmo tempo, nossa página pública na internet está em Typescript e React. Flutter Web também é uma realidade. E ainda temos um front-end público legado em Angular 2. Resumindo, há várias flores florescendo no mundo da internet em front-end (e a beleza está nos olhos de quem vê, por enquanto).

Então, toda vez que uma equipe acredita que podemos obter uma melhoria a partir de uma nova tecnologia ou abordagem, nós as experimentamos. Após o teste, às vezes, fica claro que desejamos investir e fazer uma migração completa, promovendo uma estrada de terra à estrada pavimentada. Em outros momentos, não fica tão claro, e precisamos de mais tempo e energia para decidir quais flores devem continuar florescendo e quais devemos descartar. O importante é que as equipes conheçam esses ciclos de vida dinâmicos e que geralmente concordem com o valor do aprimoramento alcançado com o alinhamento consistente em abordagens canônicas na empresa, ou, em outras palavras, o valor da canonicidade.

Considerações finais

Investir tempo em nossa estrada pavimentada, focando em menos tecnologias, gerou enormes benefícios para a forma com que os engenheiros trabalham no Nubank. Tais benefícios continuam surgindo enquanto ampliamos nossa empresa para além de centenas de engenheiros. Mas essas recompensas se devem à intencionalidade.

Em um grupo de engenheiros, há uma tendência natural e desejada de fazer escolhas baseadas nos próprios conhecimentos. Assim, se uma pessoa for 100% autônoma para decidir como e quais tecnologias devem ser usadas, as equipes acabam divergindo na forma de atuação. Por isso, precisamos ter a intenção de evitar variações não essenciais. Caso contrário, não colheremos os benefícios como uma empresa. E, ações deliberadas implicam em tomadas de decisões difíceis e conversas complicadas. Arrancar flores pelas raízes não é divertido.

Ter diversidade em como vemos o mundo, como pensamos e o que tentamos, combinado com a segurança psicológica para nos desafiarmos, é essencial para a saúde contínua da nossa empresa.

Devido à nossa dedicação em usar menos tecnologias, arriscamos formar uma “monocultura” com as tecnologias da nossa estrada pavimentada, como Clojure, Datomic e Kafka. Em uma cultura assim, as pessoas prefeririam aplaudir uma escolha de tecnologia e concordar com o grupo em vez de terem diversidade e pensamento crítico. Não queremos isso. Van Gogh provavelmente concordaria que nenhuma flor brota em uma cultura enfadonha assim. Ter diversidade em como vemos o mundo, como pensamos e o que tentamos, combinado com a segurança psicológica para nos desafiarmos, é essencial para a saúde contínua da nossa empresa.

Do lado oposto do espectro das “monoculturas e multiculturas”, vemos empresas com inúmeras fragmentações de tecnologia (por exemplo, diversas linguagens de programação com várias formas de uso). Acreditamos que isso pode acarretar uma fragmentação cultural e reduzir o desempenho. Por exemplo, pessoas de diferentes comunidades de linguagem de programação podem ter opiniões variadas sobre como estruturar códigos e serviços. A princípio, não é um problema tão grande, mas ao longo do tempo, essas pequenas diferenças sobre visões e ferramentas, combinadas com a falta de migração entre os campos, podem chegar ao ponto de se tornar duas empresas totalmente diferentes (ou seja, culturas), dentro de uma só. Construir e desenvolver uma cultura é difícil; o tribalismo complica a situação ainda mais.

Por fim, os desafios sempre mudam à medida que a empresa cresce. O alinhamento e a intencionalidade eram menos complicados (mas não eram fáceis) qu ando tínhamos 50 engenheiros, mas é diferente e mais difícil quando se trata de 600. Temos nossas questões difíceis e controversas: quanto devemos executar, quanto devemos interferir no florescimento das flores, quando levar a “estrada de terra” de volta à pavimentada. No entanto, ter uma mentalidade que evita variações não essenciais em nosso uso de tecnologias tem sido uma tremenda vantagem para a engenharia do Nubank, pois nos permite ser mais colaborativos, flexíveis e eficientes. Se continuarmos com essa mentalidade nos próximos anos, não importará se formos dogmáticos ou diferentes das outras empresas, desde que continuemos com uma bela estrada pavimentada para que nossos engenheiros trabalhem com eficiência e flores florescendo por todas as nossas estradas de terra.

Conheça nossas oportunidades