Home > Blockchain >  What is the best way to handle two async calls that must both pass and make an "irreversible&qu
What is the best way to handle two async calls that must both pass and make an "irreversible&qu

Time:03-10

I am currently wondering about this issue, I have a team to which I want to add a user (so write a new user to the database for that team) and I also want to increase the amount of users that team needs to pay for (I use stripe subscriptions).

async handleNewUser(user, teamId){
  await addUserToTeamInDatabase(user, teamId)

  await incrementSubscriberQuantityInStripe(teamId)
}

The problem is, which one do I do first? I recently ran into an issue where users were being added but the subscriber count was not increasing. However, if I reverse them and increment first and then write to database and something goes wrong in this last part, the client pays more but does not get a new member added. One possible way of approaching this is with try catch:

handleNewUser(user, teamId) {
    let userAddedToDatabase = false
    let userAddedInStripe = false

    try {
      await addUserToTeamInDatabase(user, teamId)
      userAddedToDatabase = true
      await incrementSubscriberQuantityInStripe(teamId)
      userAddedToStripe = true
    } catch (error) {
      if (userAddedToDatabase && !userAddedInStripe) {
        await removeUserFromTeamInDatabase()
      }
    }
  }

So I'm writing the new user to the database and then making a call to the stripe API. Is there a better way to approach this because it feels clumsy. Also, is there a pattern to address this problem or a name for it?

I'm using Firebase realtime database.

Thanks everyone!

CodePudding user response:

What you want to perform is a transaction. In databases a transaction is a group of operations that is successful if all of its operations are successful. If at least one operation fails, no changes are made (all the other operations are cancelled or rolled back).

And Realtime Database supports transactions! Check the documentation

CodePudding user response:

If both operations would be in the same database you'd normally bundle them in a transaction and the DB will revert to initial state if one of them fails. In your case you have operations in different external systems (DB & Stripe) so you'll have to implement the transactional logic yourself.

You could simplify your example by checking where the error comes from in the catch clause. Then you can get rid of the flags. Something like this:

handleNewUser(user, teamId) {
  try {
    await addUserToTeamInDatabase(user, teamId)
    await incrementSubscriberQuantityInStripe(teamId)
  } catch (error) {
    // If we fail to increment subscriber in Stripe,
    // cancel transaction by removing user from DB
    if (instanceof error StripeError) {
      await removeUserFromTeamInDatabase(user, teamId)
    }

    // Re-throw error upstream
    throw error;
  }
}

I use instanceof but you you change the conditional logic to fit your program.

  • Related