How to be SaaSy when one size doesn’t fit all

Gerardo Huck
Ordergroove Engineering
5 min readSep 30, 2020

In this post I will explore different techniques of dealing with feature heterogeneity as part of a SaaS B2B platform. Welcome!

The product, a tale by every SaaS company

In the beginning, your company created The Product.

You invested in The Product by adding features to it, improving existing ones, and even occasionally making a responsible call and pruning some functionality that you believed was no longer useful. You cared for it. You were good to The Product, and had big dreams for it.

The Product grew, started attracting an increasing number of clients, and they seemed to be happy with it. As time went by, you had more and more clients. Sun was shining, business was good.

Until one day, a new client came along. A special one. Maybe it was a huge client, or maybe it was the key to getting many more deals through the door; doesn’t really matter. What does matter is that this special client came with special requests.

Crossroads

Wait! You don’t want to add this requested feature to The Product, it makes no sense, and no one except this new client will ever use it.
On the other hand, it’s a special opportunity for your business… Do you have the option to refuse?

Well, you actually do. This is exactly what the folks from Basecamp decided to do. As they bluntly state it in this blog post:

If you’re a big company with special demands, we don’t want your money.

That is a bold statement I can respect! It’s also a keystone of their business model to never cross that line.

However, the line might be a little (or a lot!) blurrier in most cases. So, what can be done in this situation if you do want to take this special client’s money?

Early days: Monolithic Codebase

Wall of the Six Monoliths. Ollantaytambo, Peru

As is the case with most successful software startups, Ordergroove's platform started as a monolith: we prioritized rapid experimentation and speed to market over building a more complex distributed system and paying a Microservice Premium.

This means that in the early days of the platform we had a rather large codebase living in one single repository which contained most of the backend logic. In this post, we'll share how we dealt with client specific customizations in one such monolithic codebase.

First approach

The first concerted effort to bring order into the client specific customizations was done in the very first days of the platform, back when there were only a few paying clients using it, and our backend codebase was mostly written in a procedural style Python.

The solution used was as straightforward as it was effective: identify key places in the code where client specific logic might be needed to replace or complement core functionality, and place a few lines of code that would load and run a specific file if it existed under a "customizations/client_id/" folder. The good ol' Code Hook.

Folder Structure

One example I could recover of this (thank you git!) was the logic that computed the price of an order. The implementation assumed both tax and shipping of $0.00, but would also check if there is a client specific order_modifiers.py file with tax_total or shipping_total methods. It looked like this:

This is the code for one client that chose to only compute and charge tax for North Carolina orders.

While this technique worked fine for a while, it had some serious disadvantages:

  • all customizations for a client lived in the same folder, with no structure within it
  • sometimes it was not obvious at first glance which core functionality a given hook file belonged to, so keeping track of what specializations a given client had became quite challenging and error prone
  • granularity of the sections that supported overrides was fairly big, so client specific code often had to replicate large sections of the core logic in order to modify a few lines of code. This resulted in more copy/paste code

Over time, we realized that we needed to find a new approach if we wanted to keep scaling both the platform and development team.

OOP to the rescue

By the time reached this crossroad, our code base and our collective development practices had already evolved, and we were trying hard to come up with more modular designs, better responsibility separation, etc. We liked to think that we were writing Object Oriented Python.

We realized that we could take advantage of the Single Responsibility Principle

Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

The new approach allowed us to replicate in each specific client's folder, the same module structure of the core, including only the modules and classes that needed to be customized. Each of those classes would inherit from the core one, and only override or add the portions of logic relevant.

The decision about whether to use the stock ('core') implementation or one of the specializations was delegated using a Factory Pattern, and complexity was greatly reduced when writing or reasoning about core logic.

Juan Gutierrez, ordergroove’s Application Architect, presenting these ideas at PyGotham 2014

How much easier was it to write classes that could be modified by client specific code? Well, pretty easy I would say. All you needed to do was add our ClientSpecificFactory Mixin in the class declaration.

Overriding a piece (or all) of the logic of a core class became very straightforward: simply create a file inside the "customizations/client_id" mimicking the core path structure, and override what you want.
The same example we previously covered for NC tax computation can be done as follows

And usage of the functionality now is independent of the fact that specializations might exist or not. If you want to have an order price computed, you would use the factory method (inherited from ClientSpecificFactory class) to create the proper instance:

This approached allowed us to overcome most of the pain points of the previous attempts. Key points are

  • Logic and complexity of deciding which version (core or specialized) implemented only once (in the ClientSpecificFactory mixin class)
  • Allow any class to be specialized by simply dropping in a Mixin on its declaration
  • All specializations for a client still living on one single folder, but with the same structure of modules as the core app, so it's obvious which parts of the core logic are being modified
  • Specializations usually only need to override a few methods or attributes from core class, so no need to duplicate entire class or methods.

Mystery unveiled

Here is the full implementation of the ClientSpecificFactory mixin class that hides most of the complexity of dealing with client specific logic on this iteration of our platform.

Still hooked? We will discuss further evolution in a future post!

--

--