Having built similar applications in microservice environments, I think there are usually simpler answers than distributed transactions. And if you do need distributed transactions, this is often a sign that your service boundaries are too granular.
In fact, since the services you're describing don't know about each other, distributed transactions aren't an option.
I think the only solution to this problem is idempotency. Idempotency is a distributed systems swiss army knife—you can tackle 90% of use cases by combining retries and idempotency and never have to worry about ACID. Yes, it adds complexity. No, you don't have a choice.
I'm also not sure why this requires a lot of complexity. Can you explain how you're implementing idempotency? The simplest approach is to initialize an idempotency key on the browser side which you thread across your call graph. Stripe has built in support for idempotency keys so in that case, no additional logic is required. For providers without idempotency support, you'll need a way to track idempotency keys atomically, but this is usually trivial to implement. When a particular provider fails, you can ask users to retry.
* If you need a particular operation to succeed only if another succeeds (creating a stripe charge, for example), make sure that it runs after its dependencies.
* If you don't like the idea of forcing users to retry, you can ensure "eventual consistency" using a durable queue + idempotency.
I'm not a fan of HN comments that trivialize problems, but if you have to build complex distributed systems machinery to solve the problem you're describing, I feel strongly that something's going wrong.
Also, I think the answers you're getting keep telling you to build distributed transactions because
a.) they didn't read your post and are overindexing on "microservices"
b.) this isn't an atomicity problem, it's a workflow problem. When people say atomicity, they're usually referring to operations over data. In your case, it sounds like you need a way to coordinate execution of side-effectful operations (like creating a charge).
but, when one starts talking about idempotence and retries the first thing that comes to mind is these are just user mediated two phases commits, where an error dialog and user interaction drive the phases instead of a distributed transaction system, by which point one can just cut the middleman and build the thing out of tested and robust components
What middleman do you want to cut out? My proposal doesn't add any dependencies. It's also not possible to prevent duplicate charges if a user POSTs a form twice without idempotency keys, so idempotency is necessary.
I'm really not sure what you're actually proposing?
This approach makes a lot of sense. I was unaware of the concept of idempotency keys (nor did I know Stripe supported them). I'm actually trying to avoid complexity, which is why I turned to Auth0, Stripe (i.e. external services) to handle most of the logic for me - I'm sure I just need to figure out how to correctly apply those! Thanks for your input!
The way I have tried to implement (very likely, shoddily) idempotency is to have each mutation on an external service (say creating a customer in Stripe, corresponding to an account already administered in my database) first check if a customer with that ID already exists, and if not to create it - otherwise use the existing object (Stripe customer in this example).
The problems here are multiple, but at the very least I see the possibility of very nasty bugs if somehow the `customer_exists` call returns a false negative - this will cause the same customer to be created in Stripe twice (and thus potentially be charged twice). Another, more likely, issue is that between the `Stripe.create_customer` call and the `set_user_property` there may be an unexpected event (service/network goes down, whatever) failing to store the property - leading to a duplicate Stripe customer the next time the above code is executed. On top of that, I find it pretty difficult to reason about a code base chock full of this type of logic (perhaps that is just my personal limitation though!).
In fact, since the services you're describing don't know about each other, distributed transactions aren't an option.
I think the only solution to this problem is idempotency. Idempotency is a distributed systems swiss army knife—you can tackle 90% of use cases by combining retries and idempotency and never have to worry about ACID. Yes, it adds complexity. No, you don't have a choice.
I'm also not sure why this requires a lot of complexity. Can you explain how you're implementing idempotency? The simplest approach is to initialize an idempotency key on the browser side which you thread across your call graph. Stripe has built in support for idempotency keys so in that case, no additional logic is required. For providers without idempotency support, you'll need a way to track idempotency keys atomically, but this is usually trivial to implement. When a particular provider fails, you can ask users to retry.
* If you need a particular operation to succeed only if another succeeds (creating a stripe charge, for example), make sure that it runs after its dependencies.
* If you don't like the idea of forcing users to retry, you can ensure "eventual consistency" using a durable queue + idempotency.
I'm not a fan of HN comments that trivialize problems, but if you have to build complex distributed systems machinery to solve the problem you're describing, I feel strongly that something's going wrong.