Date

I've been thinking about a non-existent language for evaluating expressions on multiple data resources. It's key properties are:

  • Limited capability
  • Easily interpreted
  • Global references to cross-application data models
  • Logic owned by the backend

The fastest way to grok what I'm proposing is through a code snippet. Imagine this JSON object representing health potions in a game. Imagine the game syncs with a central server. In this example the predatory in-game salesman has configured the pricing such that health potions cost more if the player is below half health.

[
  {"item": "heatlh-potion", "cost": 50, "available": "(>= $player.health 50)"}
  {"item": "health-potion", "cost": 75, "available": "(< $player.health 50)"}
]

The application code might look like:

var availableItems = store.items.filter({ eval($0.available) })

When evaluated the available string will substitute the health property on the registered player resource with the current value. Next it evaluates the lisp-y expression by comparing that value to 50. In the end the value is either true or false and this is passed to our normal Swift filter function.

Motivation

Why do something like this? Sure, it provides some flexibility, but is that really so useful? Yes. This approach translates nicely to a microservice architecture.

First, let me provide some historical context to our fictitious game. In our game there are two resources $player and $store. Initial versions served both resources from a monolith. The client always requested both resources at once. When that was the case it was easy to ensure the player always saw the right items. The Monolith would filter the set of items before returning them to the client.

As time passed the resources were split into separate microservices, each exposing its own API. Perhaps clients called these directly. Perhaps an aggregator was introduced. (This is where you might invent something like GraphQL). No matter the mechanism, we're now providing subsets of data. You tell the backend what fields you want, and it'll give them you.

But things get messy as more and more fields depend on each other. How does the client know which subset of fields to request? What if the client forgets to request the $store while requesting the $player? That could lead to bugs! To solve the problem we could go back to requesting everything at once, but we all know what would happen. The app would slow down, battery life would drop, and users would complain. Even if we were to correctly manage data dependencies, there will be other obstacles. Rumor has it, some features are starting to optimistically update state locally. How do you keep it all consistent!?

Solution

Here's the beauty of our JSON from above. By moving evaluation of the available field to the client we've decoupled the meaning of our data from its age. We can refresh the $player resource every day and the store once a week and there's no risk of users seeing invalid items.

[
  {"item": "heatlh-potion", "cost": 50, "available": "(> $player.health 50)"}
  {"item": "health-potion", "cost": 75, "available": "(<= $player.health 50)"}
]

There are other benefits.

  • ETag caching becomes more effective since resources don't change as much.
  • Clients use less bandwidth under these relaxed constraints
  • Decoupled backend services no longer make calls to each other
  • Expressions can be updated to include new dependencies with just a data change. These updates apply without requiring a new client version.

Open Questions

There are a ton. Does this language only include boolean expressions? Can it compute numbers? What about list or set operators? Stringy keys in maps? How does it handle other use-cases (removing a purchased item from the shop)? Can we nest evaluations? etc.

I don't have the answers. If you found this interesting let me know. If you read this and a use-case came to mind, let me know. Just send me a toot on mastodon: @hpincket@fosstodon.org