Join us in building a fintech company that provides fast and easy access to credit for small and medium sized businesses — like a bank, but without the white collars. You’ll work on software wiring million of euros, every day, to our customers.
We’re looking for both junior and experienced software developers.
The state of the art in writing Rails applications has evolved somewhat over time. Skinny controllers, fat models was the first major shift, responsible for focusing controllers on HTTP and moving business logic to models. Then came “service objects” and, of course, micro-services — and the push back that using vanilla Rails is good enough. In the end, what we’ve all been trying to avoid is building god objects, junk drawers and spaghetti code. In other words: a place for everything, and everything in its place.
After some experimentation, the pattern we’ve landed on at Floryn is contexts and commands. It consists of grouping business logic into context modules, and providing a small top-level API as commands. This is best illustrated with an example:
# lib/bank_accounts.rb
module BankAccounts
def self.enable_bank_account(bank_account)
# ...
end
def self.disable_bank_account(bank_account)
# ...
end
end
This is a simple module, exposing two methods to the outside world to enable or disable a bank account. Unless truly trivial, these methods would typically be implemented using command objects:
# app/contexts/bank_accounts.rb
module BankAccounts
def self.enable_bank_account(bank_account)
EnableBankAccount.new.call(bank_account)
end
end
# app/contexts/bank_accounts/enable_bank_account.rb
module BankAccounts
class EnableBankAccount
def call(bank_account)
# ...
end
end
end
This code would typically live in a app/contexts
directory. But the namespace can still be used in other app
directories to organise components:
# app/models/bank_accounts/bank_account.rb
module BankAccounts
class BankAccount < ApplicationRecord
end
end
# app/jobs/bank_accounts/enable_bank_account_job.rb
module BankAccounts
class EnableBankAccountJob < ApplicationJob
def perform(bank_account)
BankAccounts.enable_bank_account(bank_account)
end
end
end
The Active Record pattern explicitly combines business logic and persistence in a single object. The ActiveRecord library Rails provides is one of the reasons why Rails is such a productive framework. Using contexts and commands, you don’t have to throw out the baby with the bath water: ActiveRecord models can still contain business logic, provided it is tightly coupled to its data. Contexts provide a natural place for more non-ActiveRecord models and logic not tightly coupled to the model’s data.
We do try to keep any dependencies between ActiveRecord models and contexts going in a single direction: our higher-level context commands can use lower-level ActiveRecord models, but not the other way around.
Not everything the application does can fit into discreet contexts. Some models cut across context boundaries. That is fine, and we have a bunch of models in app/models
that simply live in the top-level namespace, as is the Rails default.
Even with top-level namespace models, we can take care to group functionality by context. For example, a context could define a model concern:
# lib/bank_accounts/customer.rb
module BankAccounts
module Customer
extend ActiveSupport::Concern
included do
has_many :bank_accounts
end
end
end
We can then use that in our top-level Customer
model:
class Customer < ApplicationRecord
include BankAccounts::Customer
end
We see several benefits in organising our code into contexts.
First, code that belongs together is grouped together. That makes it easier to understand all the moving parts involved in a particular subdomain — especially when there many such subdomains, such as in our majestic monolith.
Second, code is given some context (pun intended). Rather than merely defining some ActiveRecord associations or some service objects, it is easier to understand what particular domain a piece of code serves.
Third, writing a context module with a discreet public API nudges developers in the right direction of thinking about capabilities and separation of concerns. It nudges us towards better-designed code.
Fourth, the discreet context API provides a nice front, beyond which the refactoring of the “internals” of the context becomes easier.
We do still face some issues with this approach. Some contexts might get intertwined, which is not always immediately obvious on the surface. Contexts do not fit very naturally in the default Rails project structure, leading to files that technically belong to a context being spread over many different files and directories on disk. And since it’s not a Rails convention, it takes a bit of effort to get existing team members and newcomers up to speed with how things work. Finally, writing new code following this pattern always feels a bit like overkill in the beginning — while at the same time, not writing it leads to it becoming harder to add after the fact. We’re still experimenting with how best to deal with these issues.
Our codebase is constantly in flux and full of imperfections. But as our development continues, our efforts tend to converge to this pattern of contexts and commands. Even in a majestic monolith, we’re slowly but steadily creating a place for everything, and putting everything in its place.
Arjan has been with Floryn since 2021 and besides his role as lead engineer he is also the self-appointed head of dad jokes. He mostly works remotely from Helmond where he lives with his wife and two daughters.
Ask Arjan about:
Floryn is a fast growing Dutch fintech, we provide loans to companies with the best customer experience and service, completely online. We use our own bespoke credit models built on banking data, supported by AI & Machine Learning.