State Management for serverless cloud-native app

by Joel Grenon, published on 2021-04-08

Managing state is the most challenging aspect of any system, even more when you want to adopt a serverless approach

I'm currently working on release 1.0 of the LiquidScale platform, which is based on my experience with micro-services state management in complex enterprise systems. You can find the rationale on the web site, but basically, storing state in database is not enough and makes things very complex as you evolve and support multiple versions of your services serving multiple versions of your apps in the field. I'm using this platform right to develop a few other systems, migrating my coaching service site to it and it also serves as foundation for my R&D work on Reflyx OS.

The following code is the definition of a simple managed scope that will be deployed as a scalable stateful component in Kubernetes. It represents a chatroom system that will manage multiple secure rooms and let people join, leave and post messages. Not a very complex system, but the goal here is to demonstrate how LQS is managing the state of these rooms and how we leverage functional micro-services to monitor, mutate and query the state of the system.

The state is managed through multiple scopes:

  • samples:chatroom: This is the root scope of the system. It manages a list of rooms and retrieve users from a global security scope. This actually creates a kind of single sign-on approach where all users are defined in a shared scope managed externally to the system.
  • lqs:security: This is the scope where all users and permissions are maintained. For this sample, this is a simple store, but this scope could abstract a complete security systems using LDAP or OIDC.
  • chatroom:room:${id}: This scope manages the state of a single room. It is dynamic because the id is used at creation time, so multiple room scopes will exists in the system. Note that only one scope implementation is deployed to the cluster (chatroom:room), but it will manage multiple scope instances and scale horizontally when needed.

Here's the complete code for the samples:chatroom scope:

export default provide(config => {
  const chatroom = config(system("samples:chatroom"));
  chatroom.schema({
    properties: {
      name: { type: "string" },
      version: { type: "string" },
      rooms: { type: "array", items: { $ref: "chatroom:room:*" } },
      users: { type: "array", items: { type: "subscription", $ref: "lqs:security", target: "default", selector: "$.users" } },
    },
    required: ["name", "version", "rooms", "users"],
  });

  chatroom.initializer(async function (state, { scope, config, schema, Collection }) {
    state.users = scope.subscribe(schema.getField("users"));
    state.name = config.get("system.name");
    state.version = config.get("system.version");
    state.rooms = new Collection();
  });

  chatroom.finalizer(async function (state, { scope }) {
    scope.unsubscribe(state.users);
  });

  return chatroom;
});

As you can see, we use a declarative approach to express scopes. In essence, scopes are reactive domain fragments that will handle incoming actions and queries, while being observable by any allowed components, including effects, conditional components or other scopes. This approach let the system assemble the right state context for any service execution, simplifying the service development, making it context agnostic in addition of being stateless.

An action would look like this :

/**
 * open-room action
 */
export default {
  key: "chatroom/open",
  bind: { scope: "lqs/chatroom" },
  description: "Create a new chatroom. This will spawn a new room scope in our cluster",
  schema: {
    properties: {
      id: { type: "string" },
      description: { type: "string" },
      visibility: { type: "string", enum: ["public", "private"], default: "public" },
      inviteCode: { type: "string" },
      owner: { type: "string" },
    },
    required: ["owner", "id"],
  },
  permissions: [{ if: ({ state, actor }) => state.users.find(m => m.username === actor), hint: "no-system-access" }],
  reducers: [
    async function openRoom({ data }, state, { helpers, scope, Buffer }) {
      // initialize room
      data.openedAt = new Date();
      data.members = [{ username: data.owner }];
      if (data.visibility === "private") {
        data.inviteCode = Buffer.from(helpers.idGen()).toString("hex").substring(0, 6).toUpperCase();
      }

      // Launch room scope and keep a reference to it in our rooms collection
      state.rooms.push(await scope.spawn("chatroom/room/${id}", data));
    },
  ],
};

This action will bind to the lqs/chatroom scope, asking the system to assemble the scope state according to the provided context( client app, user, locale, etc). This action is triggered by the client app when the user click on Create Room but actions can be triggered through various other means like timers or a set of rules or if configured as a scope effects, sensor or smart contract. This action comes with its own reducer, but reducers could be installed separately and many reducers may be triggered for a specific actions, there order being resolved by the system through priority and a few other rules. Applying an action on a scope will trigger reducers, which will in turn produce a new version of the scope state that will trigger all connected effects, sensors, rules, etc.

You can also execute reactive queries on scopes, which will be rerun if the new state as impact on the query result. This way, React apps can simply create queries on one or more scopes and receive live updates, refreshing all dependent components automatically. This is true for React app, but also for any kind of connected client apps.

All these components are managed by the platform inside Kubernetes, scaled on-demand, with a complete built-in health check system, running test suites to verify the integrity of all deployed systems.

You can find the whole chatroom sample source code and instructions on how to execute it on the LiquidScale Github. Don't hesitate to ping me if you're interested in learning more about this approach, how to leverage these concepts in your organization. We're building our community right now, have a look at my upcoming events to learn more about the upcoming LQS platform and how you can contribute!

Want to know more about this topic? Want to explore your options?Schedule a Risk-free Coaching Session