Blog
/
Announcement

Announcing: Atomic API Operations

Published
June 23, 2022
Last updated
December 7, 2023
Gadget now supports transactionally consistent atomic increment and decrement operations for any numeric field in your app!

Typical web backends tend to process write requests the same way: they first read some data from the database, change it according to the incoming parameters in memory, and then write the changed data back to the database. This works great most of the time, but if write requests like this are happening at the same time, it is race-y!

If requests are coming in one at a time, each request will be processed in order and read the most up-to-date data from the database. But if two requests start around the same time, one can accidentally overwrite the other’s writes. The potential race sequence looks like this:

  • both requests arrive around the same time
  • both read the same data from the database
  • both requests update their copy of the in-memory data to a (different) new value
  • one request finishes first and writes its data back to the database
  • the other request finishes second and writes its data to the database.

The second request reads data before the first request finishes writing to the database. This means that the second request is unaware of the changes made by the first request and will erase them! Yikes!

In most user-facing scenarios this is generally okay – if two users update a blog post’s title around the same time, one of them wins the race and the blog title is set. That’s fine because the result is the same as it would be if the two requests were processed at different times, although maybe a little strange for the user whose writing was overwritten. But in other situations, like account totals or analytics counters, dropped writes can really break stuff!  The database builders have a solution for us though: atomic operations that are processed inside the database instead of in-memory. 

Starting today, each Gadget app’s API supports atomic, in-database operations which avoid overlapping writes! Atomic operations are available under the <inline-code>_atomics<inline-code> key of any internal API update mutation and are consistent within Gadget’s existing action transactions.

For example, if we want to count the number of times a customer has placed an order on an e-commerce store, we can add an Order Count field to the Customer model and then atomically increment the counter when an order is placed:


// effect code on an Order model
module.exports = async ({api, record}) => {
  await api.internal.customer.update(record.customerId, {
    customer: { 
      _atomics: {
        orderCount: { increment: 1 }
      }
   }  
}

Each call can pass multiple atomic operations for different fields and/or multiple operations for the same field. In a ticketing application, we could use atomic operations to increment the sold ticket count and decrement the available ticket count at the same time for example:


// effect code on a Event model for a sellTicket action
module.exports = async ({api, record}) => {
   const ticket = await api.ticket.create({ticket: { event: { _link: record.id } });
  await api.internal.event.update(record.id, {
    event: { 
      _atomics: {
        soldTicketCount: { increment: 1 }
        availableTicketCount: { decrement: 1 }
      }
   }  
}

You can read more about the Atomics API in Gadget for an example app here.

Ensuring data consistency in the face of simultaneous writes is a deep topic with a lot of nuance – but Gadget makes it easy with these high-level, always-available primitives for fearlessly scaling your application.

Harry Brundage
Author
Reviewer
Try Gadget
See the difference a full-stack development platform can make.
Create app
No items found.

Announcing: Atomic API Operations

Gadget now supports transactionally consistent atomic increment and decrement operations for any numeric field in your app!
Problem
Solution
Result

Typical web backends tend to process write requests the same way: they first read some data from the database, change it according to the incoming parameters in memory, and then write the changed data back to the database. This works great most of the time, but if write requests like this are happening at the same time, it is race-y!

If requests are coming in one at a time, each request will be processed in order and read the most up-to-date data from the database. But if two requests start around the same time, one can accidentally overwrite the other’s writes. The potential race sequence looks like this:

  • both requests arrive around the same time
  • both read the same data from the database
  • both requests update their copy of the in-memory data to a (different) new value
  • one request finishes first and writes its data back to the database
  • the other request finishes second and writes its data to the database.

The second request reads data before the first request finishes writing to the database. This means that the second request is unaware of the changes made by the first request and will erase them! Yikes!

In most user-facing scenarios this is generally okay – if two users update a blog post’s title around the same time, one of them wins the race and the blog title is set. That’s fine because the result is the same as it would be if the two requests were processed at different times, although maybe a little strange for the user whose writing was overwritten. But in other situations, like account totals or analytics counters, dropped writes can really break stuff!  The database builders have a solution for us though: atomic operations that are processed inside the database instead of in-memory. 

Starting today, each Gadget app’s API supports atomic, in-database operations which avoid overlapping writes! Atomic operations are available under the <inline-code>_atomics<inline-code> key of any internal API update mutation and are consistent within Gadget’s existing action transactions.

For example, if we want to count the number of times a customer has placed an order on an e-commerce store, we can add an Order Count field to the Customer model and then atomically increment the counter when an order is placed:


// effect code on an Order model
module.exports = async ({api, record}) => {
  await api.internal.customer.update(record.customerId, {
    customer: { 
      _atomics: {
        orderCount: { increment: 1 }
      }
   }  
}

Each call can pass multiple atomic operations for different fields and/or multiple operations for the same field. In a ticketing application, we could use atomic operations to increment the sold ticket count and decrement the available ticket count at the same time for example:


// effect code on a Event model for a sellTicket action
module.exports = async ({api, record}) => {
   const ticket = await api.ticket.create({ticket: { event: { _link: record.id } });
  await api.internal.event.update(record.id, {
    event: { 
      _atomics: {
        soldTicketCount: { increment: 1 }
        availableTicketCount: { decrement: 1 }
      }
   }  
}

You can read more about the Atomics API in Gadget for an example app here.

Ensuring data consistency in the face of simultaneous writes is a deep topic with a lot of nuance – but Gadget makes it easy with these high-level, always-available primitives for fearlessly scaling your application.

Interested in learning more about Gadget?

Join leading agencies making the switch to Gadget and experience the difference a full-stack platform can make.