Designing Coaching Session Booking System with Reactive Workflows
Let's explore how the coaching session booking process benefits from LQS state management
My self-service booking system manages coaching sessions from their reserved state to their completion, review and archiving. These sessions can live for about 3 to 4 weeks. During this time, a few events can occur that will change the state of these sessions and at the same time, the state of my agenda and all associated accounts (zoom, google, etc). The typical events that may or will occur during a session lifespan are:
- cancelled - Of course, life happens and your coaching session might not be your top priority (it should though!). Cancel session actions are triggered by either the coach or the customer and have critical impacts on payments and agenda.
- started - When a session starts, our terms of service indicates that customers have 10 minutes to join or the session will be fully charged and closed. So our process needs to track join events from Zoom and update the state of the session, potentially triggering some notification logic. If the coach didn't join, a full refund and even a free additional session will be provided to the customer. This logic needs to happen in the context of a session. Coach rating might be affected, which may trigger other coach tracking logic... A lot of conditional stuff here.
- joined - Both coaches and customers have joined, sessions is active!
- finished - Session has been closed. After a few minutes, let's send a thank you note to the customer and ask for a review
- reviewed - Customer post a review of the session and coach. Again this should trigger other logic to update the coach profile and overall service satisfaction metrics. Bad review might trigger a one-on-one meeting with the coach.
- invoices - generate an invoice for this session and send it to the customer with all payment infos. Will trigger accounting tasks to properly record taxes and income in the right accounts! Of course, in our world, accounts are scopes!
- archived - time to cleanup our session system and record whatever we need about this session.
There might be other events, but this is good enough to show that we have a lot of conditional tasks around session tracking and that coding this using traditional micro-services will produce a complex system with many potential issues. We could use an HTTP api that would trigger events in a message queue and have servies listen on specific queues to perform their tasks. But we need to monitor timeouts and store the state in a database, this session state must be provided to each message handler, etc. Using a procedural workflow engine, would force us to create a bunch of workflows, all started when session is confirmed and monitoring events and state to see if conditional are met. You would have a cancellation process, a session-open process, an invoicing process, all with timers or connected to event queues, each one maintaining or reloading the state from the database or from an external micro-service.
This is the session creation process. I could design many other processes for each event or join them into a single process, which would bind a lot of logic together, which is not wanted. The real problem is the fact that the create process have to know all sub-processes to start and launch them for each sessions. They need to share some kind of context that must be provided to each sub-process when they are created. We could have custom logic to automatically launch these processes when certain events occurs, but that would add an extra layer of services to maintain.
Designing this system with LQS reactive approach, I would create 5 scopes:
- coaching - this would be the system scope defining a few policies and integration points (security, transport, etc). This scope would spawn all other children scopes like coach, agenda and session.
- coach - This is a scope associated with a coach user. It provides access to this coach agenda (subset of the global agenda?), history, rating, etc.
- customer - This is a scope monitoring the state of a customer in the system. Session history, payments, ratings, etc.
- agenda - This is the central agenda managing all sessions. Other scopes will subscribe to subsets of this state (coach, customer, etc). agenda manage timeslots and references to all active sessions (pending, active and completed)
- session - The session scope manages the actual state of a single session for the whole 3 - 4 weeks. This is where most of the action happens.
These 5 scope types are stateful components deployed in kubernetes and managed by the LQS operator. In LQS, scopes are either subscribed, mutated or queried. Each scopes supports a number of builtin publications, actions and queries but can be extended (if allowed) dynamically by deploying functional micro-services bound to this scope to enrich and add logic to the scope, without having to modify the scope implementation. The state is still controlled by the scope and strongly validated using schemas. LQS provides a set of production testing rules to keep the system stable, which we'll discuss in another post.
I won't go into all scopes for now, but the principle would be that these scopes are "spawned" by the coaching system and are actively managed by LQS and Kubernetes. They could be kept in memory but most of the time, scopes are persisted and dormant until an action or a query is received. LQS will rehydrate the scope, execute the action or query and return the scope to sleep, thus minimizing the required resources. In a 3 - 4 weeks span, a typical sessions would be in memory only maybe for 5 minutes or so total, so no need to keep all these sessions around.
With these scopes in place, our web app would trigger actions like create or cancel, which are usually triggered by a human actor. These actions would be applied to the target scope (which would be agenda in both cases). create would be reduced by the scope into a session being attached to a timeslot and a new child scope would be created as an effect. Cancel would only mark the session has cancelled. This small state change would be propagated to all interested subscribers, including coach or customer scopes and bound functional micro-services that would receive the new state and execute their rules to see if they should execute. For example, a simple fullRefund service (function) would be triggered if the cancel occurs at least 48 hours before the session planned time. Additionally, partialRefund would be triggered if session is cancel or if customer has not joined after 10 minutes (no-show) and would use system-wide partial refund ratio (ex: 50%) and execute the refund. These two functional services don't know each other and they are self encapsultated. They can be though as an executable business rule, bound to a scope and performing whatever execution they need like triggering an effect, an action or a custom logic like calling the stripe api. Of course, Stripe payments could be abstracted away by more scopes, but that's outside the scope of our system for now!
My goal is to use this system as another tutorial to help developers better understand how to design systems using LQS approach. I will create a public Github repo with most of the components in place, keeping a few internal business logic private. This will help also demonstrate how scopes can be reused and how we could potentially create an ecosystem of reusable scopes and business logic components to cover 80% of common business cases.