In mid-2018, here at Creditas, we began to wonder if the development stack we were using (Ruby) was an ideal solution to our problems…
The business difficulties
Essentially, the problems revolved around maintaining and evolving an extremely complex and ever-changing domain. Something commonly found in the environment of a startup is rapid learning related to the business and its evolution. At Creditas it wasn’t any different; our business was constantly changing in its early startup days, sometimes almost completely.
At the time we had a monolithic application written in Ruby using the Hanami framework, with the purpose of helping develop applications using DDD (Domain Driven Design). With the way our business evolved and changed, this monolith was getting more and more outdated. The monolith could not keep up with our rate of change. Our codebase was a few steps behind in relation to our business, and refactoring the monolith to reflect the company’s ubiquitous language and bounded contexts was no longer a trivial task.
The Technical Difficulties
Given this context, you can imagine the frequency of refactoring we had to deal with in our day to day life, as well as the amount that would be necessary in the changes to come, both in the monolith itself and in the microservices that were being created through feature extraction. We use Domain Driven Design to help us express and cope with these changes, which is, and always has been, great for us. However, our effort wasn’t being channeled solely to understand and model the complexities of our domain. We had to waste a lot of time with tasks that were purely technical. The chosen languages and frameworks weren’t helping! At that moment we decided to hire developers with experience in DDD, as well as in other development stacks, to help us deal with the complexities of the business. These developers started to question and criticize the way we worked with DDD in Ruby, which motivated the response of one of them to a question from Stack Overflow entitled “Why does DDD seem only popular with static languages like C# and Java?”. These doubts and criticisms revolved around concerns over efficiency, such as: the difficulty of refactoring without the help of tools; type-checking to avoid writing unnecessary unit tests; and, above all, a lack of frameworks for simple and decoupled dependency injection, object-to-object mapping, ORM , etc.
Let’s take a deeper look at this last example: ORMs. In DDD it is common practice to use the repository pattern to persist domain entities. In the internal implementation of repositories, ORM frameworks are often used to save entities in the database. That was exactly what we wanted to do.
A brief history of persistent data structures using repositories in Ruby
Some time ago, when we had the insight that DDD could help us handle the complexities of our domain, we started to look for tools that were less opinionated than Ruby on Rails (what we were using). This was how we found Hanami, which came with a less opinionated mindset and that allowed a higher level of decoupling on the project “layers”, in addition to facilitating questions of tactical design patterns of DDD.
We made several PoCs using the framework, validating what would be the organization of the code, how tests would run, which gems would be used in each layer, configurations, the separation of bounded contexts, etc. All the experiments were presented to the developers (which at the time were only 10) and together we decided to take the risk of proceeding with the tool that was in its BETA version (that’s right, beta). Over the next few years we hosted a number of meetings here at Creditas about Hanami and wrote a series of posts (in portuguese) sharing what was learned.
Although Hanami had some differences, as it was the only framework we found at the time that implemented the Repository Pattern in Ruby, over time we began to face problems with performance and a lack of tools to facilitate some types of tasks. We tried to upgrade Hanami (which already had a stable release), but we noticed that the framework was going down a different path from what we were used to and thought was ideal. To give some examples, creating an entity in Hanami involves some specifics like inheriting from a Hanami::Entity, which makes it impossible to create a “PORO” domain, and its constructor must be hashed. Also, we could not easily persist our aggregates when they were composed of other Entities and Value Objects, there was an excess of boilerplate code to perform such a task.
That’s when we started studying other frameworks…
NOTE: The Hanami framework is still used in the company (in this monolith) and to this day we are one of the main sponsors of the project, that despite not meeting our expectations, was part of our history and brought us here! ❤
Following the situation with Hanami, we started looking at other new possibilities in the Ruby ecosystem. One solution was to purely use Sequel for persistence as it was already used by Hanami behind the scenes (no new dependencies would be needed, what would be nice).
After we did a PoC that turned out alright, we noticed it would require writing a lot of new code, which we wanted to avoid since the starting point. Not only was a lot of code needed to create persistence, but more code was also needed for testing.
Sequel also implements another form of persistence as Active Record, but Active Record Pattern is another style of persistence which doesn’t have a good synergy with the style described in DDD, making it difficult to separate the domain layer with the persistence layer (we’ll get into that in a second).
The next PoC we did for Ruby was done with ROM. The result of using ROM in the PoC was relatively satisfactory and resolved many of our worries about writing lots of code to achieve the expected persistence results, but there was a problem: ROM had already broken backwards compatibility with previous versions 3 times in a short period. We noticed that it wasn’t a very mature project. Also, the community didn’t talk much about the library and even during PoC it was tricky to find solutions on the internet.
Due to the problems described, we carried a very high risk using ROM in our projects. The library could cease to be maintained at any moment and we would have a huge technical debt to resolve.
Active Record (Ruby on Rails)
Active Record is a Rails module responsible for implementing the Active Record pattern in Ruby. No doubt it is the most widespread and well-known lib that implements this pattern in the Ruby universe, but as stated earlier, the Active Record pattern is different from the Repository pattern.
We will allow ourselves to delve into this here. Some of our requirements for entity and repository modeling, based on DDD’s tactical standards, were:
- Domain entities should contain only business logic, without knowing persistence rules.
- Associations between entities should not corrupt the boundaries of bounded contexts.
- The repository should be the glue that abstracts and joins entities with the database. Therefore, entities can not be saved without using a repository.
To validate these and some other items we decided to make a POC. In its conclusion, although we had found a model that reasonably addressed our problems, it did not efficiently address the rules above. For example:
- The modeling of aggregates could not be self-contained with respect to business rules. An example is the
<<operator, which adds a new item to a list. The attribute, in addition to adding to the list, also performs an automatic insert in the database each time it is invoked, violating the rule that entities should not be persisted without a repository.
- The save method implemented by Repository could be accessed directly by Entity, as they both shared the same Model. The same goes for other standard methods like
Note that these are not problems with implementing Active Record in Rails (or Sequel), but intentional aspects of the Active Record pattern, which do not fit well with our pattern of repositories.
Even with these problems we were not quick to discard the option. Although we had noticed some problems in the implementation model we developed in this PoC, we thought it made sense to proceed. The strategy allowed us to leverage our Ruby knowledge and reduce boilerplate that didn’t add value to the business. Thus, we created a new Rails microservice, containing a specific and well-defined bounded context. The architecture used was very close to what we set up in this sample repository. As of the date of this article we have this application written and running on RoR without any major problems.
However, even though we decided to start this new application in RoR , we were still not happy with the fragility of our solution. We had to “turn a blind eye” to many things, using processes (such as pull requests) to validate and “manually” prevent errors that were permissible in our architecture. This was one of the last efforts we put into trying to stay with Ruby, and it was the moment where we decided to take our search outside the Ruby stack to find alternatives.
The end of the line for Ruby?
This summary of “data persistence using repositories” is part of our history of difficulties and attempts to implement DDD in Ruby. We could still exemplify difficulties with Dependency Injection and other everyday tasks that are solved with much more complexity compared to other languages.
But OK, if Ruby doesn’t seem to be the most appropriate language and ecosystem for us, what could it be?
Continue reading in our next post
Our goal in Product Technology is to create the best solution to our customers’ needs by offering the most complete and innovative portfolio of financial products from financing to investing. We’re here to build a fully automated and intelligent digital platform and we’re looking for people who’ll join us on that: https://jobs.kenoby.com/careers-creditas