I will be honest. The first prototype of Factorial was built with Phoenix (Elixir). Nothing is more exciting to a crafter than to try a new tool. This one will be different, better perhaps. But it was not the time for tinkering: our goal was to launch Factorial's first version under one month. We threw the prototype to /dev/null
and turned to the tool we knew best, Ruby on Rails.
Spoiler: We launched our minimum viable product and successfully built a company around it.
The most important priority for a technical founder is the speed of learning. You have a limited amount of time – let's be honest, cash in the bank – and you need to find product/market fit. But the biggest challenge is to learn fast while establishing a solid foundation, layering engineering principles to build upon. Nothing too specific nor rigid. Just a handful of concepts paired with some early abstractions that illustrate them. After all, the first lines of code you write will surely disappear, but the founding principles may prevail. Tweaked, twirled and twisted, but still there.
So, what were those principles in Factorial?
First Principle: Verbs as first-class citizens
Most programs are modeled after human language constructs. Objects are nouns and methods verbs.
Hence, if we say:
Hellen rejects John's last time off request
We tend to imagine the program like such:
class User
has_many :time_off_requests
def last_time_off_request
time_of_requests.last
end
def rejects(time_off_request)
time_off_request.update(rejected: false)
end
end
hellen = User.find_by(email: 'hellen@example.com')
john = User.find_by(email: 'john@example.com')
hellen.rejects john.last_time_off_request # Reads like english, yai!
With time you start to realize that this is the wrong way to go about it.
As much as we feel nice and cozy when a program reads like English, it is not a good architectural principle. It's easy to confuse familiarity with good design.
Rails abuses the English language metaphor. I've always been amazed that one of the pillars of the framework is the omnipresent Inflector, a class that is responsible for the singular/plural forms which all the Rails conventions are based upon. And don't get me wrong, the English metaphor is probably one of the reasons Rails succeeded. It felt magical yet familiar to beginners, but in my experience, it falls apart in complex domains.
In Rails nouns are glorified and belong to the realms of ActiveRecord
. Please all stand-up and show respect to the king of all subjects, the almighty User
model. After all, most sentences in your applications are in the form of user <verb> <predicate>
.
But verbs? They are an afterthought. To represent verbs we use methods in models and controllers. Both implementations are hard to reuse, one for being coupled to the database and the other with HTTP framework abstractions. And then you have strong parameters in the middle coupling both to make things worse.
At Factorial we decided early on that we wanted verbs to be first-class citizens. When we explored a new domain we've asked ourselves which verbs would be part of it. We've resisted the temptation to model everything under CRUD/REST conventions and instead embrace domain-specific verbs. One does not say: "Hellen updates John's last time off request with rejected = false" but instead "Hellen rejects John's last time off request".
We've implemented this by introducing a new type of object called the Interactor. This is not a new concept at all, but we've embraced it to the point that our applications do not look like a Rails application anymore: Our ActiveRecord
models are very small, they don't use any callbacks and most of the business logic is implemented on the Interactors instead. We've also removed the need for strong parameters since controllers never talk to ActiveRecord
directly. The verbs in our application are reusable, and more importantly, composable.
Second Principle: Share memory, split domains
We started Factorial at the peak of the microservices hype. "Microservices" encompassed a lot of different concepts, from scaling to organization topologies. Most of the ideas revolved around a very well known problem:
As applications grow in size complexity increases exponentially making it harder to operate (for both machines and humans).
The microservices approach is to impose a paternalistic constraint on how to build software. It mostly bans shared memory so developers can't "cheat" the boundaries on their systems. Some people even go as far as to ban shared databases to keep domains completely isolated. By creating strict boundaries you ensure developers behave accordingly. We love constraint-based architectures but we wanted to explore another set of trade-offs. Can we have non-paternalistic boundaries?
We've modeled our application as a monolith (sacrilege!) but separating each domain in a different engine. Those are our boundaries, soft but effective. No hardware constraints, no ban o shared memory and most importantly, no ban on shared databases. Our domain is highly relational so it is critical for us to be able to commit transactions across domains and sacrificing joins is not an option yet.
It's the developer's job to ensure that boundaries are respected. I would be lying if I told you that we do. Sometimes we violate them knowingly (strictly speaking, incurring technical debt), others unknowingly. And yet, I believe the trade-off is a sweet spot for Factorial.
Our rule of thumb is: "If we decide to remove this feature, how easy would it be to delete its code?". Optimizing for code removal early on has compounding benefits in the long run. And trust me, we've removed a lot of features until finding product/market fit.
Third Principle: Everything must be audited
Some of the tools and techniques from the present are shaped by constraints from the past. This is very obvious when you work with relational databases. They were built with a space constraint that does not hold true anymore: Storage prices have decreased from $30,000/GB in 1989 to $0.019/GB in 2018 source.
This constraint is the reason why most relational databases are mutable. They don't hold all the historical information. They update or delete rows when they are instructed to do so. A notable exception is the binlog which is mostly used for replication purposes (although is becoming more common to connect it to a data sink to be able to gain back that information).
We decided early on to store a record for each verb that was invoked. Yes, storage is cheap and we wanted to be as verbose as we could. We wanted to know what users have done with the system so we could solve their issues and eventually ask temporal questions: What was the salary of that given employee two months ago? Can you get all the contract changes in the last month?
This turned out to be one of the most difficult but rewarding principles to implement. We've discovered that we could also use those audit events as a mechanism for domains to communicate with each other. Auditing events became the glue between the first and second principles. A verb triggers an event that can be listened to by another domain. Beautiful!
But this was actually not our invention. Without knowing it, we've stumbled upon Domain-Driven Design. Most of these concepts have been explored during the last decade: Verbs are shaped after the ubiquitous language, domains are bounded contexts and audit events are common among event sourcing and event storming practitioners.
Conclusion
The take here is that principles are important but must be vague enough. People joining your organization will come with new ideas. If they find rigid and strict principles they won't be able to influence the team. At Factorial we hire engineers from different backgrounds and it had a very positive effect on our engineering practices: Each developer has brought their own experiences from other languages and ecosystems and built new tools on top of those vague and flexible foundational principles. It's so enriching.
Top comments (3)
This was a really interesting read. Thanks for writing it! I think people tend to downplay the importance of language in coding and how it helps us understand how things were designed. So it's cool to see something about those ideas.
I've used services and decorators before and it sounds like that might be similar to Interactors. I would love to hear more about what the Interactor is.
Hi Dan,
An Interactor is just a glorified method. Basically a class with a
call
method that performs a verb. In our specific case though, we've built some sort of abstraction so all our interactors return a "Result" (like you would in rust) to communicate how that user intention succeded or not. Also, our interactors always leave an event (we use railseventstore.org) behind for auditing and inter-domain communicationOkay, yeah I've used something similar called ActiveInteraction. github.com/AaronLasseigne/active_i...
It also returns a result object similar to what you are talking about.