There are many solutions that compromise more than I'm comfortable with. Granted, if your use case is complex, such as moving money between different banks, more pleasant alternatives may be impossible. But let's look at what we can do in the common scenario, where the use of microservices interferes with our would-be database transactions.
Option 1: Avoid the need for transactions if it all possible
Obvious and mentioned before, but ideal if we can manage it. Did the components actually belong in the same microservice? Or can we redesign the system(s) such that the transaction becomes unnecessary? Perhaps accepting non-transactionality is the most affordable sacrifice.
Option 2: Use a queue
If there is enough certainty that the other service will succeed at whatever we want it to do, we can call it via some form of queue. The queued item won't be picked up until later, but we can make sure that the item is queued.
For example, say that we want to insert an entity and send an e-mail, as a single transaction. Instead of calling the mail server, we queue the e-mail in a table.
Begin transaction
Insert entity
Insert e-mail
Commit transaction
A clear drawback is that multiple microservices will need access to the same table.
Option 3: Do the external work last, just before completing the transaction
This approach rests on the assumption that committing the transaction is very unlikely to fail.
Begin transaction
Insert entity
Insert another entity
Make external call
Commit transaction
If the queries fail, the external call has not taken place yet. If the external call fails, the transaction is never committed.
This approach comes with the limitations that we can only make one external call, and it must be done last (i.e. we cannot use its result in our queries).
Option 4: Create things in a pending state
As posted here, we can have multiple microservices create different components, each in a pending state, non-transactionally.
Any validation is performed, but nothing is created in a definitive state. After everything has been successfully created, each component is activated. Usually, this operation is so simple and the odds of something going wrong are so small, that we may even prefer to do the activation non-transactionally.
The greatest drawback is probably that we have to account for the existence of pending items. Any select query needs to consider whether to include pending data. Most should ignore it. And updates are another story altogether.
Option 5: Let the microservice share its query
None of the other options do it for you? Then let's get unorthodox.
Depending on the company, this one may be unacceptable. I'm aware. This is unorthodox. If it's not acceptable, go another route. But if this fits your situation, it solves the problem simply and powerfully. It might just be the most acceptable compromise.
There is a way to turn queries from multiple microservices into a simple, single database transaction.
Return the query, rather than executing it.
Begin transaction
Execute our own query
Make external call, receiving a query
Execute received query
Commit transaction
Network-wise, each microservice needs to be able to access each database. Keep this in mind, also in regard to future scaling.
If the databases involved in the transaction are on the same server, this will be a regular transaction. If they are on different servers, it will be a distributed transaction. The code is the same regardless.
We receive the query, including its connection type, its parameters, and its connection string. We can wrap it up in a neat executable Command class, keeping the flow readable: The microservice call results in a Command, which we execute, as part of our transaction.
The connection string is what the originating microservice gives us, so for all intents and purposes, the query is still considered to be executed by that microservice. We are just physically routing it through the client microservice. Does that make a difference? Well, it lets us put it in the same transaction with another query.
If the compromise is acceptable, this approach gives us the straightforward transactionality of a monolith application, in a microservice architecture.