All Products
Search
Document Center

ApsaraMQ for RocketMQ:Transactional messages

Last Updated:Mar 11, 2026

Transactional messages are a type of featured message provided by ApsaraMQ for RocketMQ that guarantee a local transaction and a message delivery either both succeed or both fail. This two-phase commit mechanism keeps a core service and its downstream consumers in sync without the resource-locking overhead of eXtended Architecture (XA) distributed transactions.

Distributed transaction requirements

Use transactional messages when:

  • An order system must update its database and notify the logistics, points, and cart services atomically.

  • A payment service must record a debit and publish an event to downstream ledger consumers.

  • Delivering a message without completing the local transaction (or vice versa) would leave the system in an inconsistent state.

How transactional messages work

Why normal messages fall short

Combining a local database transaction with a normal message send creates a gap where one can succeed without the other:

  • The message is sent, but the local transaction fails. Downstream consumers act on an uncommitted change.

  • The local transaction commits, but the message send fails. Downstream consumers never learn about the change.

  • A timeout occurs, and neither the producer nor the broker can determine whether to commit or roll back.

Normal message solution

Why XA transactions are too costly

The XA protocol can coordinate distributed transactions across systems, but it locks resources for the entire duration of the transaction. As the number of participating systems grows, lock contention increases and throughput drops.

Two-phase commit with half messages

ApsaraMQ for RocketMQ transactional messages use a two-phase commit protocol that avoids both problems:

Transactional message solution

  1. Send a half message. The producer sends a message to the broker. The broker persists it and returns an acknowledgment (ACK) to the producer. The message is marked as *not ready for delivery* -- a message in this state is called a half message. Downstream consumers cannot see it yet.

  2. Run the local transaction. The producer runs its local database operation (for example, updating an order status from *unpaid* to *paid*).

  3. Commit or roll back. The producer reports the local transaction result to the broker:

    • Commit: The broker marks the half message as *ready for delivery* and delivers it to consumers.

    • Rollback: The broker discards the half message. Consumers never receive it.

  4. Transaction status check (recovery). If the broker does not receive a commit or rollback result -- due to a network failure or producer restart -- it sends a status query to a producer instance in the cluster. The producer checks the local transaction result and re-reports it to the broker.

Transaction status check workflow

Note

For the query interval and maximum retry count, see Parameter limits.

Message lifecycle

A transactional message moves through the following states:

Transactional message lifecycle

StateDescription
InitializationThe producer builds the half message and prepares to send it to the broker.
Transaction to be committedThe broker stores the half message in the transaction storage system. Unlike a normal message, the half message is not persisted by the broker in the standard way. The message is invisible to consumers.
Committed for consumptionThe local transaction succeeds. The broker stores the half message in the storage system, making it visible to consumers.
Message rollbackThe local transaction fails. The broker discards the half message. The workflow ends.
Being consumedA consumer picks up the message and begins processing it. If the consumer does not return a result within the configured timeout, ApsaraMQ for RocketMQ retries delivery. For details, see Consumption retry.
Consumption result commitThe consumer commits the consumption result. The message is marked as consumed but not immediately deleted.
Message deletionThe message retention period expires or storage space runs low. ApsaraMQ for RocketMQ deletes the oldest messages in a rolling manner. See Message storage and cleanup.

By default, ApsaraMQ for RocketMQ retains all messages. A consumed message is not deleted immediately -- consumers can re-consume it until the retention period expires or storage space is reclaimed.

Send transactional messages (Java)

Prerequisites

Before you begin, make sure that you have:

  • A topic with MessageType set to Transaction in the ApsaraMQ for RocketMQ console.

  • The instance endpoint (from the Endpoints tab of the Instance Details page)

  • (If applicable) The instance username and password (from the Intelligent Authentication tab of the Access Control page)

Differences from normal messages

Sending a transactional message differs from sending a normal message in two ways:

  • Transaction checker required. Register a transaction checker when building the producer. The checker runs automatically if the broker queries the transaction status after a failure.

  • Topic binding required. Bind the target topic to the producer at build time so the built-in checker can recover the transaction status.

Sample code

Build a producer with a transaction checker, begin a transaction, send a half message, run the local transaction, then commit or roll back.

Sample code

import java.time.Duration;
import org.apache.rocketmq.client.apis.*;
import org.apache.rocketmq.client.apis.message.Message;
import org.apache.rocketmq.client.apis.producer.Producer;
import org.apache.rocketmq.client.apis.producer.SendReceipt;
import org.apache.rocketmq.client.apis.producer.Transaction;
import org.apache.rocketmq.client.apis.producer.TransactionResolution;
import org.apache.rocketmq.client.java.message.MessageBuilderImpl;
import org.apache.rocketmq.client.apis.message.MessageBuilder;
import org.apache.rocketmq.shaded.com.google.common.base.Strings;

public class ProducerTransactionMessageExample {

    // Simulates checking whether the order exists in the database.
    private static boolean checkOrderById(String orderId) {
        return true;
    }

    // Simulates the local transaction (e.g., inserting an order record).
    private static boolean doLocalTransaction() {
        return true;
    }

    public static void main(String[] args) throws ClientException {
        // Replace with your instance endpoint.
        // Find this on the Endpoints tab of the Instance Details page
        // in the ApsaraMQ for RocketMQ console.
        String endpoints = "<your-instance-endpoint>";

        // The topic must have MessageType set to Transaction.
        String topic = "<your-transaction-topic>";

        ClientServiceProvider provider = ClientServiceProvider.loadService();
        ClientConfigurationBuilder builder = ClientConfiguration.newBuilder()
            .setEndpoints(endpoints);

        // Authentication:
        // - Public endpoint: specify username and password (find these on the
        //   Intelligent Authentication tab of the Access Control page).
        // - VPC endpoint on ECS: no credentials needed; the broker resolves
        //   them from the VPC.
        // - Serverless instance: always specify credentials, regardless of
        //   access method.
        builder.setCredentialProvider(
            new StaticSessionCredentialsProvider("<your-username>", "<your-password>"));
        builder.setRequestTimeout(Duration.ofMillis(5000));
        ClientConfiguration configuration = builder.build();

        MessageBuilder messageBuilder = new MessageBuilderImpl();

        // Build the producer with a transaction checker.
        // The checker runs when the broker queries a half message whose
        // commit/rollback result was not received.
        Producer producer = provider.newProducerBuilder()
            .setTransactionChecker(messageView -> {
                // Look up the order ID attached to the half message.
                // If the order exists in the database, the local transaction
                // committed successfully. Otherwise, roll back.
                final String orderId = messageView.getProperties().get("OrderId");
                if (Strings.isNullOrEmpty(orderId)) {
                    return TransactionResolution.ROLLBACK;
                }
                return checkOrderById(orderId)
                    ? TransactionResolution.COMMIT
                    : TransactionResolution.ROLLBACK;
            })
            .setTopics(topic)
            .setClientConfiguration(configuration)
            .build();

        // Step 1: Begin a transaction.
        final Transaction transaction;
        try {
            transaction = producer.beginTransaction();
        } catch (ClientException e) {
            e.printStackTrace();
            return;
        }

        // Step 2: Build and send the half message.
        Message message = messageBuilder.setTopic(topic)
            .setKeys("messageKey1")
            .setTag("messageTag")
            // Attach a business ID for the transaction checker to query later.
            .addProperty("OrderId", "xxx")
            .setBody("messageBody".getBytes())
            .build();

        final SendReceipt sendReceipt;
        try {
            sendReceipt = producer.send(message, transaction);
        } catch (ClientException e) {
            // Half message send failed. The transaction ends here.
            return;
        }

        // Step 3: Run the local transaction.
        boolean localTransactionOk = doLocalTransaction();

        // Step 4: Commit or roll back based on the local transaction result.
        if (localTransactionOk) {
            try {
                transaction.commit();
            } catch (ClientException e) {
                // If the commit call fails, the broker will invoke the
                // transaction checker to resolve the status.
                e.printStackTrace();
            }
        } else {
            try {
                transaction.rollback();
            } catch (ClientException e) {
                // Log the error. The broker will invoke the transaction
                // checker to resolve the status.
                e.printStackTrace();
            }
        }
    }
}

Replace the following placeholders with your actual values:

PlaceholderDescriptionExample
<your-instance-endpoint>Instance endpoint (from the Endpoints tab of the Instance Details page)xxx-hangzhou.rmq.aliyuncs.com:8080
<your-transaction-topic>Topic name with MessageType set to Transactionorder-tx-topic
<your-username>Instance username (from the Intelligent Authentication tab of the Access Control page)MjoxODgwNzcwODY5MD****
<your-password>Instance passwordNEh6cm9FVUl****

For complete SDK examples across all supported languages, see Apache RocketMQ 5.x SDKs.

Best practices

Minimize unknown transaction results

The transaction status check exists as a safety net for failures during commit or rollback. A high volume of status checks degrades system performance and delays message delivery. Design local transactions to return a definitive Commit or Rollback result as quickly as possible.

Handle in-progress transactions correctly

When the broker queries a half message status and the local transaction is still running, return Unknown -- not Commit or Rollback. Returning a premature result can cause data inconsistency.

If status checks arrive too early because the local transaction is slow, consider these approaches:

  • Increase the first check delay. Configure a longer interval before the broker sends its first status query. Trade-off: this also delays recovery for genuinely failed transactions.

  • Detect in-progress state explicitly. Design the local transaction logic to distinguish between "still running" and "failed" so the checker returns the correct status.

Limits

ConstraintDetails
Topic typeTransactional messages require a topic with MessageType set to Transaction.
One SendReceipt per transactionEach transaction supports only one SendReceipt.
Eventual consistency onlyTransactional messages guarantee consistency between the local transaction and message delivery. They do not guarantee real-time consistency across downstream consumers. Until the message is delivered, downstream state may lag behind the upstream transaction. Use transactional messages only when asynchronous downstream processing is acceptable.
Consumer-side responsibilityApsaraMQ for RocketMQ guarantees that committed messages are delivered, but each downstream consumer must handle processing correctly. Implement consumption retry logic to handle transient failures. See Consumption retry.
Transaction timeoutIf the broker cannot determine the transaction result after the configured timeout and maximum retry count, it rolls back the half message by default. See Parameter limits.