Desafios de desenvolvimento em uma fintech de crescimento acelerado — Parte II de II

Caio Abe
Creditas Tech
Published in
9 min readSep 12, 2019
Ilustração de uma trena com o acrônimo SOLID escrito na lateral

No primeiro post desta série, discursei sobre o nascimento da Tribo de Servicing da Creditas e como foi o processo de desenvolvimento de um novo contexto em uma aplicação em produção.

A leitura do primeiro post é essencial para entender o contexto deste, então se você ainda não o leu, comece por aqui.

Ao final do primeiro post, citei que após a criação da Calculadora tivemos o desafio adicional de extraí-la para um novo serviço e então passar o bastão para outro time chamado Pricing, cujo escopo é unificar os cálculos que nasceram espalhados pela empresa em um único lugar.

Dividi esta série de posts em dois momentos:

Parte I:

  • Contexto histórico
  • Context Map de Servicing
  • Desafios e oportunidades de um novo contexto
  • Processo de desenvolvimento
  • Outros navegantes a vista
  • Conclusão

Parte II:

  • Integração com o time de Pricing
  • Estratégia de extração (Branch by Abstraction)
  • Fatores de sucesso para a extração
  • SOLID aplicado a nível arquitetural
  • Conclusão

Integração com o time de Pricing

Na primeira parte desta série falei sobre a nossa possibilidade de adotar uma nova linguagem para solucionarmos os problemas de domínio do novo contexto da Calculadora que nascia em Servicing.

As principais linguagens consideradas foram Scala pela sua natureza Functional First, Python pelas suas libs matemáticas e Kotlin pelo seu ecossistema e concordância com outros movimentos já existentes na Creditas.

Felizmente, decidimos pelo pragmatismo, postergando a decisão de sair do Ruby para quando realmente tivéssemos mais certezas de negócio e também de qual rumo a empresa tomaria em relação às novas stacks.

Dado que o custo de criarmos um novo contexto dentro da aplicação já existente era mínimo naquele momento, seguimos com Ruby.

[GIF] Mão com luva ornamentada compassadamente desdobra tecido revelando um Ruby em forma de coração

Os demais times na Creditas seguiram a mesma mentalidade de tomar decisões com cautela e visão sistêmica, principalmente em um momento de muitas definições na empresa. Desta forma, o time de Pricing também nasceu em Ruby naquela época, o que foi muito oportuno na redução de custos para todos nós.

Isto nos proporcionou muitas facilidades, tais como inexistência de curva de aprendizado técnico entre times Ruby, agilidade por não termos que “traduzir” uma linguagem para outra e até mesmo questões de infraestrutura pois já tínhamos conhecimento maduro sobre como provisionar um ambiente de produção estável em Ruby.

Apenas alinhamentos de negócio e de estrutura do nosso projeto foram necessários para fazermos essa passagem de bastão do contexto para Pricing.

Fatores de sucesso para a extração

A quê devemos o sucesso de termos atendido uma demanda complexa, entregando tanto o valor comportamental quanto estrutural da aplicação sem perder oportunidades de negócio?

Me aproprio aqui de uma citação do nosso Tech Leader Rafael Manzoni, que conduziu as entregas do time:

A minha visão é que sim, o SOLID nos proporcionou dinamismo e desacoplamento dos cálculos, facilitando a configuração de cada componente. Mas o sucesso da extração na minha visão foi o uso conjunto dos padrões estratégicos e táticos do DDD, tais como o Context Map e a Integração entre contextos com o uso de ACLs (Anti-Corruption Layers) que tornaram tudo mais fácil.

Diagrama de sequência que exemplifica de forma simplificada a comunicação entre contextos via Api

Note que se quiséssemos trocar a forma de comunicação de HTTP para uma chamada interna ou para qualquer outro protocolo de troca de mensagens, bastaria injetarmos uma implementação diferente do Gateway, sem grandes esforços.

Adicionalmente à citação do nosso líder, acredito que o sucesso da criação de um contexto desacoplado e sua extração com facilidade se deve a soma de todos estes fatores citados até aqui e também a mais um ingrediente especial:

Um time que — acima de interesses pessoais — joga para vencer em união, considerando não apenas a solução técnica para a Squad ou para a Tribo, mas sim para a empresa como um todo.

Listo aqui algumas reflexões desafiadoras que surgiram ao longo desta jornada:

  • Como o contexto de Cobrança acessaria a Calculadora com baixo acoplamento?
  • Onde é o limite entre criar complexidade excessiva nessas integrações? Adiar decisões ao custo de possíveis débitos técnicos ou adiantá-las ao custo de desenhar abstrações possivelmente erradas?
  • Como serão realizados os testes de integração nas camadas de aplicação de Cobrança? Confiabilidade com duplicação versus Confiança com DRY?
  • Como será o desenho dos Casos de Uso da calculadora, dado que ela pode virar uma API no futuro?
  • O que diabos é Integração entre Bounded Contexts e por que ninguém me deixa codar em paz?

Brincadeiras à parte, foi graças ao know how de pessoas desenvolvedoras experientes, muitos debates e um longo caminho trilhado em nosso Grupo de Estudos de DDD que conseguimos aprender e aplicar tudo isso.

Se interessou pelos desafios e pelo grupo de estudos? Vem bater um papo com a gente.

Estratégia de extração (Branch by Abstraction)

Iniciamos a extração fazendo uma cópia deste contexto para um novo serviço em Rails API, que seria acessado pela nossa aplicação principal via chamadas HTTP. Assim teríamos a oportunidade de aprender como a integração se comportaria em um novo cenário.

Tudo parecia muito bom até que, logo de início, novos desafios surgiram: Confiabilidade e Latência.

[GIF] Meme do pobre guaxinim que cai em profundo sentimento de desilusão ao verificar o comportamento de diluição de seu estável bloco de açúcar na superfície seca quando em contato com um novo ambiente (água)

Tivemos que lidar com Confiabilidade pois o serviço era novo e deveríamos nos assegurar de que os cálculos não falhariam em nossos processamentos diários. Decidimos então seguir com uma estratégia de Branch by Abstraction, onde basicamente substituímos a chamada da calculadora por uma abstração que nos permitisse utilizar tanto o fluxo novo quanto o antigo em casos de falha do primeiro. Basicamente, todas as classes que faziam uso da Calculadora na camada de aplicação do nosso code base passaram a utilizar um serviço e dois gateways:

  • Gateway externo, que realiza o cálculo fazendo uma chamada HTTP
  • Gateway interno, que chama o controller da cópia local da Calculadora

Desta forma, a utilização do nosso componente de Fallback que realiza o chaveamento entre fluxo legado e novo ficou mais ou menos assim:

# service.rb@fallback_wrapper.new(main: @http_calculator_gateway,fallback: @internal_calculator_gateway,error_event_name: ‘installment_calculation_failed’).perform(dto)

O componente de Fallback envia a mensagem perform para o gateway HTTP, passando um DTO (Data Transfer Object) como parâmetro que serve de input tanto para um quanto para o outro. Caso o fluxo principal falhe, o fluxo alternativo do gateway interno é utilizado, e a mensagem de erro ‘installment_calculation_failed’ é registrada em forma de Transactional events no New Relic.

Esta estratégia nos garantiu o funcionamento correto em ambos os fluxos e nos ajudou a garantir a confiabilidade no serviço.

Tivemos que lidar com Latência pois agora teríamos que fazer uma chamada HTTP para cada parcela. O problema é que alguns casos de uso faziam cálculos de 180 parcelas de uma vez. Nossa solução foi abstrair os gateways e serviços para que fizessem chamadas em lote. Diminuímos a latência de minutos para segundos.

Nosso contexto de Billing continua utilizando a Calculadora interna como Fallback para alguns casos, mas nosso monitoramento de erros no APM já nos dá pistas de que este fluxo não será mais necessário em breve pois os erros logados são raríssimos. Em outras palavras, poderemos deletar o código legado e depender somente do microsserviço de Pricing em breve! 🎉

[GIF] Cena editada: Rafiki arremessa Simba bebê pelo penhasco em icônica cena do filme O Rei Leão

SOLID aplicado a nível arquitetural

Os princípios foram aplicados de diversas formas ao longo de todo este processo, da criação à extração. Cito aqui alguns destaques:

Contextos de Servicing: Billing (cobrança), Onboarding (novos créditos na plataforma) e Calculator (calculadora)

SRP: Single Responsibility Principle

Mais do que simplesmente “Fazer uma única coisa”, a segregação da Calculadora e o Uncle Bob nos dizem algo mais importante: “Apenas uma razão para mudar”. Dado que este módulo foi isolado para um novo contexto, os interesses de negócio voláteis e experimentais da Calculadora (ex.: novas políticas a cada semana) não interferiram em responsabilidades de Billing e Onboarding (entrada de novos créditos). Não existiam responsabilidades compartilhadas entre os contextos e, portanto, tivemos facilidade em trabalhar em conjunto com frentes diversas no mesmo code base.

Setas apontando em direção ao domínio

OCP: Open Closed Principle

Protegemos o alto nível (regras de negócio) do baixo nível (comunicação entre contextos) através da arquitetura hexagonal. Nossos casos de uso — tais como o processamento diário da carteira e o cálculo de índice de inflação — apontavam para o Domínio e não o contrário. Assim a extração da Calculadora foi simples e não demandou alterações em nossas classes mais estáveis de Cobrança. Além disso, os componentes de cálculo das parcelas puderam ser configurados com políticas que não implicaram em alterações nas bases de cálculos. Pudemos assim estender nossas funcionalidades sem alterá-las.

Abstrações de HTTP e InternalGateway se encaixando no Serviço

LSP: Liskov Substitution Principle

Com pequenos ajustes pudemos facilmente criar Fallbacks que utilizam o mesmo contrato (interface comum com inputs e outputs idênticos) para acesso interno e externo. Em outras palavras, conseguimos facilmente substituir uma implementação pela outra e esperar o mesmo comportamento. Outro exemplo interessante é que independentemente de qual tipo de cálculo realizamos, todos os componentes Nó e Folha expõem interfaces padronizadas.

Carrinho de comida de rua que oferece açaí, pastel, website, yakissoba, acarajé e hot dog

ISP: Interface Segregation Principle

Como bem sabemos, o ISP declara que clientes não deveriam ser forçados a depender de interfaces que não utilizam. No nível do código, podemos dizer de forma simplista que “não devem sobrar métodos”. Já quando aplicado no nível arquitetural, o ISP se revela de forma mais sutil. É notável sua semelhança com o SRP, porém, ao passo que o último se refere a interesses de mudança, o primeiro se concentra em lógica de comunicação entre clientes. Isto ficará mais claro com o exemplo a seguir.

Nossa primeira versão da Service possuía três casos de uso em uma única Service: cálculo de componentes da parcela, criação do plano de parcelas e cálculo de índice de inflação. Para evitar dependências entre módulos durante a estratégia de estrangulamento do fluxo antigo para o novo — reduzindo complexidade e mitigando a probabilidade de riscos — segregamos as interfaces em partes mais granulares (três serviços) para seguirmos com a extração de forma incremental e muito mais segura.

Três entidades conversando. Acima: “Eu orquestro”, Esquerda: “Faz você!” e Direita: “Me injeta aí!”

DIP: Dependency Inversion Principle

Ao invertermos a direção do controle, garantimos que as camadas de negócio da nossa arquitetura continuassem sem saber nada sobre como ou quem fazia os cálculos. Injetamos as estratégias de chamadas HTTP via gateway e de chamadas internas via controllers internos, dependendo apenas de abstrações e, portanto, invertendo o controle. Desta forma, processos em lote foram encapsulados para não adicionarmos complexidades nas camadas de orquestração (Application). Além disso, o padrão de Repositórios aplicado — mesmo que à moda do Ruby, que não possui um container de injeção e interfaces com tipagem forte — é um ótimo exemplo de como passamos o controle de baixo nível para estas classes, evitando que nosso domínio saiba sobre estratégias de persistência.

Conclusão

Aqui eu finalizo esta série de posts que discute de forma não exaustiva a aplicação dos princípios de desenvolvimento de sistemas que atacam problemas de domínios complexos em constante evolução, com rigoroso cuidado na tomada de decisões.

Espero ter promovido reflexões construtivas e concretizado um pouco deste mundo abstrato dos livros com exemplos reais que encaramos por aqui.

Postem seus cases nos comentários para aprendermos mais juntos!

Foto das ilustrações originais utilizadas como capa para a postagem

Sob o chapéu de um dev-ex-designer, entre um código e outro ainda faço alguns rabiscos. É claro que todo trabalho de natureza iterativa — escrita de código inclusa — gera alguns rascunhos prévios à versão final. Escolhi a ilustração do SOLID na trena em oposição ao SOLID como arquitetura neste post para ilustrar que mesmo com tantos papos arquiteturais, continuamos usando nossas ferramentas fundamentais do dia-a-dia como exímios pedreiros de software.

Obrigado pela leitura e até a próxima galera!

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.

--

--