We are just selling a few products to our
customers, it can’t be that hard, can it?
Background
We were a number of months into designing and building a micro service
based platform to replace a number of monolithic B2B applications (each had
their own business organisations, products and services), when it was
discovered that no one in the business had considered how the
products might be sold and billed from a single place, they just assumed we
would do it the same as we had always done.
This seemed like a simple issue, cross selling products to our customers is
what we had always done, but in a very manual way.
We were planning on combining each business into a single data-driven
platform where we would link a product to a customer and invoice them for the
number of users that use the product.
Investigations
However after doing some research around the different business heads and
sales teams, it turned out that there wasn’t a lot of common ground to start
from.
In reality:
- Customers were being charged different prices for
the same products.
- Prices were time boxed or volume controlled.
- Some sales were based on sliding scales.
- Contracts sometimes didn’t mention pricing.
- Price changes were difficult due to no end dates in
contracts.
- Forecast were impossible as we sometimes billed in
arrears.
- The number of drivers changed on a nearly daily
basis.
There were also a few additional wrinkles that I found out about that
sat outside of what we knew:
- Pricing, some customers were being charged in Euros
and a few in other currencies.
- Content, was predominantly in English, but we had
one product that supported 5 foreign languages.
- Versioning, some content was updated constantly and
only some customers would be eligible to see it.
- Users being added at any time should get the latest
content their company has access to.
- Users being added or removed might trigger a charge
or refund depending on the contract.
This new information left us a with a lot of new technical challenges
that had not been picked up in the investigation stages.
What we did as a business
Our generic business model was simple, our customers have drivers and
their drivers have risk associated with them. We mapped and showed companies
where that risk was and how to mitigate it.
Based on a bespoke customer algorithm they might have a range of products added
to their user account depending on what they do:
- a regular licence check
- elearning courses
- online assessments
- on road training
- telematics installed in their vehicle
We charged the customers for each product their drivers completed.
The billing was very ad-hoc and was purely manual
Our monthly bill run was taking weeks of time and no one really had any solid
idea of where the money was coming from or when it might arrive.
Automation
I really wanted to limit the manual work needed to run the new platform
and decided to introduce a workflow process that would cover the assignment and
billing for products to drivers.
In later iterations the workflow became intrinsic and central to how the entire
platform functioned as we linked any repeatable task as a step in a workflow
somewhere.
I tasked one of the senior engineers to find some options and after some demos
and discussions we ended up using an off the shelf c# open source platform
called elsa: https://v2.elsaworkflows.io/
It had hooks in and out of our systems for triggers and had its own admin UI
tooling, this saved us a lot of development time.
Mobile Contracts
I decided that the best solution for "how will we sell and bill
products to our customers" was to build it like a mobile phone
bundle/contract.
Everyone understands how these work and I had done this before in the telco
world.
I was hopeful it would allow us to sell any number of items bundled together to
any number of customers at any price.
I set out the technical business requirements for the Offer Package.
It would be a structural wrapper around our content, products, prices,
policies, templates and any other data linked to
what we would “sell” to a customer.
The technical design sessions did become busy over time as the team and I
were modelling against ideas that were evolving.
To help we developed proof of concepts and mock-ups as we went to make sure
we were on the right track.
We were able to hide the complexity and repetitive work inside the workflow
engine.
Data Structure
Working closely with the my data team, we worked out a base data
structure that would work within a relational database and fit into the overall
tech roadmap we had in place.
Our biggest goal was that we didn’t want any data duplication anywhere, as it
was a huge issue in the current monoliths.
The Offer package structure ended up a:
Each Offer Package had many Products and each Product had versioned content
in different languages with a default price for each available currency.
We added more availability dates after modelling this out against what the
sales teams would need to sell we realised we had to be able to control when
the Offer package would be available.
These were then added to Products, Prices, Pricelists and in the end every
entity in the database had them, these became some of our core fields that we
used for all sorts of reporting and automation checks.
Building the validation for this to work in code was a headache for the
developers and a lot of unit tests were created to cover a very large amount of
use-cases.
The validation had to consider cases like: An offer package could be assigned
to a customer at any time, but it would only be visible to a workflow to be
assigned to a user after its available date was passed. However, if the
products or prices inside the Offer Package were not available then the
workflow would error and the users would not be assigned the right products and
no charges would be raised.
As the complexity grew so did the technical workshops and whiteboard
sessions to make sure we were not missing anything.
Educating the developers on what the architecture looked like was
imperative, proof of concept models and UI were being drafted/built on a
sprintly basis.
Once the team had agreed on a model that felt we were on the right track
and user stories were created
Demos were done to the stakeholders at the beginning of each testing
release.
Any ideas from these stored for a later development phase to get the core
done as quickly as possible.
Versioning
The next challenge was how to store the versioned and multi-lingual
product content, each type of content had a different data structure; text,
videos, images, files, full assessments, 3rd party data,
external API calls.
A standard relational database model really wasn’t the best option for storing
this mixed data set very efficiently, but we couldn’t move from this.
One piece of content like a word document or PDF
for example, would need to be made available to a driver in their own language,
this content would always need to be up to date when they read it the first
time. However, if it was something they signed and agreed to, they should never
see the updated version of the same document as it would not be valid.
But if its not something that’s been agreed, then maybe they should see the
latest version every time, even if its changed.
This gets more complicated when managing elearning content, if you have started
the course and it gets updated after you started, should you get the new
version or continue on the old?
All of these sorts of use cases had to be managed via flags in the data,
every workflow decision needed to be able to be answered via a data query.
Complex Content Storage
Each product was available in multiple languages, each language could have
multiple versions.
Some versions were ready for the future, some had been available in the
past but only one was ever available ‘now’.
Now being relative to when the user wanted to view it.
My initial solution was to use a SQL field to store serialised and
structured model JSON for the content and standard fields for the generic data
name, dates, version ID, etc.
I had done this sort of thing before for health data models in XML in the past
and although the developers weren’t overly comfortable with storing the data
like this, once we ran a proof of concept through they saw the benefits.
Each product type would have its own model in c# that would allow the system to
store and read the JSON instantly based on a lookup type stored in the data.
It also enabled us to update the structure of the models by either migrating
the JSON itself or simply creating a new model type to save and read the new
data type.
Unfortunately, when we started volume testing this as part of a proof of
concept, it didn’t work exactly as we expected.
For smaller pieces of content, like a URL to a file etc, both reads and writes
were fast. The serialised JSON would be ingested by the correct class and it
worked really well.
However when working with complex data like assessments/surveys, large amounts
of HTML this read time became too long.
We had to split the versions of each content out so these also had available
from/to dates as well.
This wasn’t a huge amount of extra work, just an extra layer of data and it now
left us with a much cleaner data structure, we still used the JSON concept, but
now we could find the just content we needed ‘now’ much faster than opening a
large piece of JSON, parsing it and finding the right ‘now’ content.
Cache time
After the table adjustment we ran some more efficiency/benchmarking
tests on the new data layers and found that storing the complete UI view in the
table as serialised JSON was extremely fast.
Adding a caching layer on top of the database behind the API gave us even more
flexibility.
If we needed [/product/123/content/568] from the API it returned the JSON
object directly from the cache if it was available, or from the database if
not, while storing the result in the cache for next time.
The UI could render the object without any computation time.
A workflow service monitored the cache and clearing out anything that was older
than a set time frame.
We added a separate mechanism that reacted to changed data in the database and removed dirty
cache entries.
Take away
Looking back there were times when I am sure my team thought I was losing the
plot with some of the decisions that were being made, however the end result
was a slick system with the smallest amount of human admin as necessary to keep
things running.
There are parts of this I have not mentioned, that were just as big a challenge
as offer packages, like billing cross currencies, the definition of ‘now’ for 2
users in different timezones etc.
The Offer Packages and Workflow were a turning point in the evolution of how
software was being developed.
Abstracting business ideas away into generic data structures with automation
and solid technology behind them leaves everyone in a place where any
mis-understandings or changes can be made without re-developing code and moves
closer to my personal goal of configuration
Don’t get me wrong, it wasn’t perfect, but it was a lot cleaner and more
importantly leaner than the systems we were leaving behind.
Complexity
The underlying data over time became very simple and was everything
ended up being based on a generic table structure.
The models we mapped out in the database were mapped directly to real world
values, giving the data meaning and context, end to end across the platform and
its services.
When we reviewed the system when it was up and running with real data in
it, we could see that things made sense to both developers and business
stakeholders alike.
Any changes to the system now had to have a reason for being made, new
fields have to have a value for being added.
We were finally only storing the data we really needed.
What I didn’t get time to do
This wasn’t a true micro service when I left because although the
services themselves (Offer Package, Prices, Billing, etc) were independent
pieces of functionality running on different azure pieces, there was still only
a single database.
Workflow was the only true microservice, although we shared c# code with it
so it could understand our logic.
The SQL database schema was set out to allow splitting or sharding, but we
were waiting until we had the data volumes in before we started doing this as
the cost was just too high to do on a small customer base.
Future thoughts
Abstract data-driven systems are powerful, but they are also very complicated
to put together.
We could have built a very specific piece of software, this would give us a
mirror of what we were replacing, it was a bold move to do and took longer than
we wanted it to, but the end result is a data driven platform, that with a few changes
of config could be used for a completely different business, without
redevelopment.