Transactions are units of work that you want the database to treat as an indivisible whole: either all of the database changes take place, or none of them. Gadget now offers built-in transaction support, as well as the ability to toggle transactions on and off, as needed.
It’s not uncommon to write code that touches multiple pieces of data at once. Real world processes often tend to need this: when you purchase from an online store, the order is created at the same time as inventory being deducted. You don’t want to deduct the inventory if the order doesn’t go through (e.g. payment failure). To support this, backends typically use database transactions that wrap a set of changes and commit them to the database at the same time.
When two or more actions are invoked together in a single API call, Gadget wraps them in a database transaction which guarantees they’ll be atomic. For example, if we were to make an API call to create a blog post record and related image records (stored in a separate, related model), we would use Gadget’s nested actions as follows:
If either of our blog post images fail to save, say because a validation deemed the image to be too big, then the whole transaction will abort and neither the blog post nor the image records will be saved. This saves you from having to write any clean up code, because your database never commits the changes unless all of the changes are successful.
In the Gadget framework, the Run, Success, and Failure effect stacks each come with their own transaction boundary. By default, Run Effects are transactional, whereas Success and Failure Effects are not. You can change the transactionality of any effect stack in Gadget by toggling the transaction icon.
Gadget transactions have a 5 second timeout, in order to preserve scalability. This should be long enough for most Actions, but if you’re working with external APIs or high volumes of data, it can be convenient to turn off this transaction to avoid the timeout, and instead manage transactions yourself with the explicit <inline-code>api.transaction<inline-code> wrapper.
Full transaction support in Gadget’s API is unlike many of the backend-as-a-service systems you see on the market today. Transactions are hard to implement securely for platforms, and hard to scale as applications get bigger, but they are really important for building scaling software. There are workarounds that avoid the need for transactions, but they require developers to bend over backwards to accommodate the underlying database, spending a huge amount of time worrying about data consistency and fixing bugs instead of building features.
Gadget implements transactions using websockets. When you write a code effect that works with the <inline-code>api<inline-code> object in Gadget, that object is communicating with the backend over a stateful websocket connection, instead of making HTTP calls for each individual operation. The websocket allows us to ensure that the same backend database connection is used for all the work one request might do, and to ensure that we properly close or abort database transactions when things fail. Websocket connections reuse the same GraphQL schema generated for each app, exposed over HTTP, so that everything you might want to do with the <inline-code>api<inline-code> object works both inside or outside a transaction.
We use <inline-code>graphql-ws<inline-code> to power our GraphQL-over-websocket implementation, and it’s been quite a bit of work to productionize this approach. Stateful connections like this are always a pain in the modern world of stateless cattle-not-pets services. We’ve invested heavily in our load balancing approach, database scalability, and tracing setup to ensure we can scale transaction throughput as Gadget grows.