The first post in the series can be found here.
Last week I've presented one aspect of complexity of the transactional part of our platform - availability (when trying to book a new appointment). And what is more important, how one modelling trick has significantly reduced coupling & simplified (and distributed) business logic.
Today I'll cover another aspect of the same functionality (appointments) - pricing. And again, my goal is to show you how one Eureka moment has corrected the course of our architectural direction for the whole platform (!).
Baby steps
Initially our pricing logic was trivial - each service type (e.g. haircut) could have a different base price, associated with this service (1:1). Adding the concept of service variants didn't change much - each variant had its individual base price, but that was about it.
As the platform is global, we've added flexible tax configuration mechanism - this has of course increase the complexity a bit. So did various discounting options: starting with basic percentage/flat amount cost reduction, ending with group discount for packages or manually overridden "special prices".
We've also started developing various marketing campaign features - obviously segment-targetting offers had their own discounting rules as well.
Complexity grows
As you can see, there's a lot of moving parts & just to be clear - the end-user is not presented with all the pricing variants for a particular service. It's the platform that goes through all of the combinations to pick up the one which is the most beneficial for the end-user. No weaseling, fair business that helps & counsels salons and their clients as much as possible.
So every time someone is about to make a booking (either from salon's perspective or as a marketplace user) all these complex gears were shifting, sweating, taking all the pricing conditions under consideration & outputting the human-readable ... prices.
One service variant = one price.
One day I took my family to visit a new Georgian restaurant nearby. We like trying foreign cuisine and the menu on restaurant's website looked interesting. We were welcomed by the owner himself (of Georgian origin), we had a chit-chat, he has provided some recommendations and he has handed us nice-looking, freshly printed menus. Probably due to nice conversation, it was the first time in a while when I grabbed these cards with the full awareness (not mechanically, already thinking about what I want to order).
And somehow ... a single question popped up in my head: how would this look alike if he was ... our platform?
The mystery of Georgian chef
If this doesn't make sense to you, let me clarify - menu is the list of offered items/services (in our case: service variants) with their prices. So the activity of him handing us menus corresponds 1:1 to when we show service variants online (on the web or in the app) so the booking can be made.
So, imagine he's our platform (LOL), we've just sat down and what does he do (keeping in mind what I've written in the 1st part of this post)?
He runs to the kitchen facilities and ... starts calculating everything from scratch - for all the dishes he has in the menu, he calculates the individual price - based on the prices on ingredients (potato, cottage cheese, olive oil, ...), equipment wearing off, local taxes, electric energy consumption, cook's effort, depth of inventory & which expiry dates are getting close ... He quickly does the math and runs back with the hand-written menu, so we can make our pick.
Next client enters the door and the owner ... would follow exactly the same routine. From the beginning, taking the very same factors under consideration - factors that are nearly constant! It would be stupid, won't it? But this is EXACTLY what our platform is doing several times per second!
Why?
Initially no-one cared about the complexity of pricing, because ... there was no complexity. Service variants just had their base prices and this is what was presented. In time the outcome on the screen became an indirect product of multi-step calculation, but this outcome didn't get its "identity" - we knew what it was (a "menu", an "offer", a "price list") but it didn't get its persistent abstraction on the platform - it was always created on-demand, on-the-go and perished immediately after usage.
Khachapuri saves the day
All we had to do is to create the concept of "price lists" (parallel to the menu in the restaurant) that can be immediately presented to the customer. "Price lists" that have to be occasionally re-generated once one of the "price ingredients" change, e.g. salon's owner decides to raise base price, taxes get a new rate, there's a new promotion, a discounted marketing offer is generated for a particular customer.
Voila!
One simple design abstraction added and ...
- one of the most frequently used API endpoints on the platform gets ultra-fast
- pricing logic is decoupled from appointment booking logic
- testability improves (e.g. by adding the ability to mock the "price lists")
- complexity is also better distributed (we don't have to access half of the system to make an actual appointment)
- salon configuration logic can be physically separated (architecture-wise) from appointment logic: which has a big, positive impact on platform resilience (because of bulk-heading)
The conclusion I'd like to share with you is that in many cases instead of thinking about purely technical optimizations ("what can I do with this query, which index should I add?"), we ought to really validate the model of the domain - maybe we're missing a critical concept which (because of its specific properties: in the case above - low volatility) can do the job if applied properly.