home bsky

Query, Mutation, Action model


Software developers like to organize functionalities with fancy names. Sometimes those fancy names define behaviors and assumptions that we try to standardize.

HTTP is one of them. It defines operation methods to an URI, like GET, POST, PUT and DELETE. Those methods allow tools like browsers to assume properties of the request and response, like idempotency and cacheability. See RFC 9110.

Those standards moved the web forward. Architectural pattern that build on top of HTTP, like REST, dictate how a big percentage of how the web works today.

While HTTP defines semantics for the communication interface between client and server, and provide clients assumptions of the server behavior, the server itself is free to implement those methods as it pleases.

I believe that this makes, in practice, the HTTP protocol be the “framework” for the backend instead of an implementation detail. How much does the server change if we change the HTTP method of an endpoint? Probably not much.

Also, using HTTP as a framework makes it easier to build stuff that isn’t reliable and future-proof. For example, cookies are considered inappropriate for REST, and assumes a browser environment.

GraphQL and tRPC are architectures that have HTTP as an implementation detail. Both can arguably provide better developer experience and performance. That is, when implemented correctly, which is hard in GraphQL.

Both have 2 types of operations, queries and mutations (subscriptions could be considered a special type of queries), and allow the protocol to take advantage of restrictions imposed on the server.

Another example of an architecture with this pattern is Convex, which is a backend framework and a backend as a service, that inspired my thoughts on this note.

Operations as a framework

Restricting what each operation can do allows certain optimizations without complicating the server implementation. The basic assumption is making queries idempotent and mutations deterministic.

That means that both queries and mutations can’t call external services, since an external service call can’t guarantee those properties.

Queries

Making queries idempotent allows:

To make them idempotent, queries can only read data, calling other queries if needed.

Mutations

And making mutations deterministic allows:

To make them deterministic, mutations can read and write data, calling queries and other mutations if needed.

Actions.

When a function can’t be made deterministic, there should be a escape hatch. This escapes the restrictions of queries and mutations, but looses the benefits of the optimizations that those abstractions provides.

That’s actions. Actions can call queries and mutations, but should not query the database directly. A restriction that treats actions as a system outside the query and mutation model, allowing the separation of safe and unsafe operations, for free.

The bigger picture

With that, we have a system where:

And that is without sacrificing the flexibility of server code. A lot of backends as a service solutions with real time capabilities talk directly to the database, using rule systems that are usually hard to scale.

Did I mention that this model composes? Not only with our own code, but can be extended with some sort of component architecture. Like PostgreSQL extensions, but allowing the client to call its operations and having the same properties of the query and mutations with isolation with the rest of the system. Can your backend do that?


I am loving Convex and how this model just clicks. Check out their blog post about been the Software Defined Database.