Trocando Ruby por Kotlin — Parte 1

Fabrício Rissetto
Creditas Tech
Published in
7 min readAug 8, 2019
Choose your weapon

Esse é o primeiro de uma série de 3 posts que foram escritos de forma colaborativa por Fabrício Rissetto, Matheus Caceres e Murilo Capanema. Não deixe de conferir as partes 2 e 3.

Em meados de 2018, na Creditas, começamos a nos questionar se a stack de desenvolvimento que estávamos utilizando (Ruby) continha o ferramental ideal para resolver nossos problemas…

As dificuldades de negócio

Os problemas giravam em torno de, basicamente, manter e evoluir um domínio extremamente complexo e em constante mudança. Algo comum no ambiente de startups é o rápido aprendizado em relação ao business e sua evolução, ou às vezes, MUDANÇA. E conosco não foi diferente, nosso business mudou muitas vezes nos primórdios da startup, algumas vezes quase que por completo.

Nossas ferramentas

Na época tínhamos um monolito escrito em Ruby utilizando o framework Hanami, que vinha com a proposta de ajudar no desenvolvimento de aplicações DDD (Domain Driven Design). Conforme a evolução e mudança do nosso business acontecia, esse monolito cada vez mais ficava para trás. Ele não estava acompanhando o ritmo de evolução. Nossa base de código estava defasada demais em relação ao negócio e refatorar o monolito para refletir a linguagem ubíqua e os bounded contexts atuais da empresa já não era mais uma tarefa trivial.

As dificuldades técnicas

Dado o contexto, você já deve imaginar a frequência de refactorings com que tínhamos que lidar no nosso dia a dia e a quantidade de mudanças que estavam por vir, tanto no próprio monolito quanto nos microsserviços que estavam sendo criados através da extração de funcionalidades. Utilizamos Domain Driven Design para nos ajudar a expressar e lidar com essas mudanças, o que é, e sempre foi, ótimo para nós. Mas nosso esforço não estava sendo canalizado apenas em entender e modelar as complexidades do nosso domínio. Estávamos gastando muito tempo com tarefas puramente técnicas. A linguagem e os frameworks escolhidos não estavam nos ajudando!

Naquele momento começamos a contratar desenvolvedores com ampla experiência em DDD, mesmo que de outras stacks de desenvolvimento, para nos ajudar a lidar com as complexidades de negócio. Esses desenvolvedores começaram a questionar e fazer críticas ao modelo de trabalho de DDD em Ruby, o que inclusive motivou a resposta de um deles numa pergunta do Stack Overflow intitulada de “Porque DDD parece ser tão mais popular em linguagens estáticas (C# e Java) do que em dinâmicas?”. Esses questionamentos e críticas giravam basicamente em torno de produtividade, como: dificuldade de refactorings sem contar com auxílio de ferramentas, checagem de tipos a fim de evitar a escrita desnecessária de testes de unidade, e, principalmente, falta de frameworks para fazer de maneira simples e desacoplada dependency injection, object-to-object mapping, ORM, etc.

Peguemos para aprofundar esse último exemplo: ORMs. Em DDD é uma prática muito comum se utilizar o padrão de repositórios para fazer a persistência das entidades do domínio. Internamente na implementação dos repositórios frequentemente são utilizados frameworks de ORM para salvar as entidades no banco de dados. E era exatamente isso que queríamos fazer.

Uma breve história sobre persistência de dados utilizando repositórios em Ruby

Hanami

Há um bom tempo atrás, quando tivemos o insight de que DDD poderia nos ajudar a lidar com as complexidades do domínio, começamos a procurar ferramentas que fossem menos opinativas do que o Ruby on Rails (que era o que estávamos utilizando). Foi então que encontramos o Hanami, que vinha com uma proposta menos opinativa e que permitia um alto nível de desacoplamento das “camadas” do projeto, além de facilitar algumas questões dos padrões táticos do DDD.

Fizemos várias POCs com a utilização do framework, validando como ficaria a organização do código, como seriam feitos testes, quais gems seriam usadas em cada layer, configurações, separação de bounded contexts, etc. Todos os experimentos foram apresentados para os desenvolvedores (que na época eram apenas 10) e em conjunto decidimos comprar o risco de seguir com a ferramenta que estava em sua versão BETA (isso mesmo, beta). Nos anos seguintes sediamos diversos meetups na Creditas sobre Hanami e escrevemos uma série de posts compartilhando os aprendizados.

Apesar do Hanami ter alguns diferenciais, como ter sido o único framework que encontramos na época que implementava o Repository Pattern em Ruby, com o passar do tempo começamos a enfrentar problemas com performance e falta de ferramentas que facilitassem alguns tipos de tarefas. Tentamos atualizar o Hanami (que já possuía release estável), porém notamos que o framework estava seguindo um caminho diferente do que estávamos acostumados e que achávamos ideal. Para dar alguns exemplos, criar uma entidade no Hanami envolve algumas especificidades como herdar de um Hanami::Entity, o que impossibilita a criação de um domínio “PORO”, e seu construtor obrigatoriamente deve receber um hash. Além disso, não conseguíamos persistir nossos agregados com facilidade quando eles eram compostos de outras Entities e Value Objects, havia muito boilerplate para executar tal tarefa.

Foi aí que começamos a estudar outros frameworks…

OBS: O framework Hanami ainda é utilizado na empresa (nesse monolito) e até hoje somos um dos principais sponsors do projeto que apesar de não atender mais nossas expectativas, fez parte da nossa história e nos trouxe até aqui! ❤

Sequel

Após os eventos com o Hanami, começamos a olhar novas possibilidades no ecossistema Ruby. Uma solução era usar o Sequel puro para persistência, pois ele já era usado pelo próprio Hanami por baixo dos panos (não seria necessário adicionar nenhuma nova dependência).

Fizemos uma POC e ficou legal, entretanto exigia escrever muito código, o que gostaríamos de evitar desde o início. Não só era necessário muito código para criar a persistência, mas também era necessário mais código para os testes.

O Sequel implementa também uma outra maneira de persistência como Active Record, mas o Active Record Pattern é um outro estilo de persistência, que não tem uma boa sinergia com o estilo descrito no DDD, dificultando a separação da camada de domínio com a persistência (aprofundaremos nisso logo em seguida).

ROM

A próxima POC de persistência em Ruby que realizamos foi feita com o ROM. O resultado da utilização do ROM na POC foi relativamente satisfatória e resolvia boa parte da questão de escrever muito código para atingir os resultados esperados de persistência, mas tinha um problema: o ROM já havia quebrado a compatibilidade com versões anteriores 3 vezes em muito pouco tempo. Notamos que ainda não era um projeto muito maduro. Também, a comunidade não falava muito sobre a biblioteca e mesmo durante a POC foi complicado achar soluções na internet.

Então devido aos problemas descritos, tínhamos um risco muito alto na utilização do ROM em nossos projetos. A biblioteca a qualquer momento poderia parar de ser mantida e teríamos um débito técnico enorme para ser resolvido.

Active Record (Ruby on Rails)

Active Record é um módulo do Rails responsável por implementar o padrão Active Record em Ruby. Sem dúvida nenhuma é a lib mais difundida e conhecida que implementa esse padrão no universo Ruby, mas como dito anteriormente, o Active Record pattern é diferente do Repository pattern.

Nos permitiremos dar uma aprofundada nisso aqui. Alguns dos nossos requisitos para modelagem de entidades e repositórios, baseado nos padrões táticos do DDD, eram:

  1. As entidades do domínio deveriam conter apenas lógica de negócio, sem conhecer regras de persistência.
  2. As associações entre as entidades não deveriam corromper os limites dos bounded contexts.
  3. O repositório deveria ser a cola que abstrai e une as entidades com o banco de dados. Portanto, entidades não poderiam ser salvas sem o uso de um repositório.

Para validar esses e alguns outros itens decidimos fazer uma POC. Na sua conclusão, apesar de termos encontrado um modelo que atendesse razoavelmente bem os nossos problemas, não resolvia de maneira eficiente os itens citados acima. Por exemplo:

  1. A modelagem de Aggregates não conseguia ser autocontida no que diz respeito à regras de negócio. Um exemplo é o operador <<, que adiciona um novo item numa lista. O atributo, além de adicionar na lista, também performa automaticamente um insert no banco toda vez que é invocado, violando a regra de que entidades não devem ser persistidas sem um repositório.
  2. O método de save implementado pelo Repository poderia ser acessado diretamente pela Entity, pois os dois compartilhavam a mesma Model. O mesmo vale para outros métodos padrões como o find, exists?, first, last, take.

Note que esses não são problemas da implementação do Active Record no Rails (ou Sequel), e sim aspectos intencionais do padrão Active Record, que não encaixam bem com o padrão de repositórios.

Mas mesmo com esses problemas não fomos radicais em descartar a opção. Embora tivéssemos percebido alguns problemas no modelo de implementação que desenvolvemos nessa POC, achávamos que fazia sentido prosseguir. A estratégia nos permitia aproveitar nosso conhecimento em Ruby e diminuir a quantidade de boilerplate que não agregava valor ao negócio. Sendo assim, criamos um novo microsserviço em Rails, contendo um bounded context específico e bem definido. A arquitetura utilizada foi bem próxima da que montamos nesse repositório de exemplo. Até a data deste artigo temos essa aplicação escrita e rodando em RoR sem grandes problemas.

Porém, mesmo decidindo começar essa nova aplicação em RoR, ainda não estávamos contentes com as fragilidades da solução. Tivemos que fazer “vista grossa” para muitas coisas, utilizando processos (como pull requests) para validar e impedir “manualmente” erros que eram permissivos na nossa arquitetura. Esse foi um dos últimos esforços que dedicamos para tentar nos manter em Ruby, e foi o momento definitivo em que começamos a dar maior seriedade em procurar alternativas fora da stack.

Fim da linha pro Ruby?

Esse resumo sobre “persistência de dados utilizando repositórios” é, na verdade, uma parte da nossa história de dificuldades e tentativas de se implementar DDD em Ruby. Ainda poderíamos exemplificar dificuldades com Dependency Injection e outras tarefas do dia a dia que são resolvidas com muito mais complexidades se comparado a outras linguagens.

Mas OK, se Ruby não parece ser a linguagem e ecossistema mais adequado para nós, qual poderia ser?

Continue a leitura no próximo post.

Tem interesse em trabalhar conosco? Nós estamos sempre procurando por pessoas apaixonadas por tecnologia para fazer parte da nossa tripulação! Você pode conferir nossas vagas aqui.

--

--