Sobes.tech
Middle+

How would you implement sending a message to a client considering the possibility of transaction cancellation?

sobes.tech AI

Answer from AI

To send a message to a client with the possibility of transaction cancellation in Java, I would apply the Outbox pattern.

  1. Database transaction:

    • The message is saved in a special "outbox" table within the same transaction as the main processing logic.
    • If the transaction is rolled back due to a business logic error, the record in the outbox table is not committed, preventing the message from being sent.
  2. Outbox Processor:

    • A separate process (e.g., a background daemon or scheduler) periodically polls the outbox table for new, unsent messages.
    • This process retrieves messages from the database.
    • Sends messages to the client (e.g., via HTTP, message queue, WebSocket, etc.).
    • Upon successful sending, the message is marked as sent in the outbox table or deleted.
  3. Idempotency:

    • It is important that the client-side handler is idempotent so that resending a message (in case of Outbox Processor failures) does not lead to undesirable consequences.

Example structure of the outbox table:

Field Type Description
id UUID Unique identifier
payload JSON Message content
type VARCHAR Message type (e.g., "order_created")
created_at TIMESTAMP Record creation time
processed_at TIMESTAMP Processing time (NULL if not processed)
status VARCHAR Status (e.g., "PENDING", "SENT", "FAILED")

Pseudo-code example:

// Business logic
public void processOrder(Order order) {
    try (Transaction tx = connection.beginTransaction()) {
        // Main logic: save order
        saveOrder(order, tx);

        // Create outbox record within the same transaction
        OutboxEntry message = new OutboxEntry(UUID.randomUUID(), orderToJson(order), "order_processed", Instant.now(), null, "PENDING");
        saveOutboxEntry(message, tx);

        tx.commit(); // If failure occurs here, outbox record will not be saved
    } catch (Exception e) {
        // Transaction will rollback, outbox record will not be added
        log.error("Error processing order", e);
    }
}
// Outbox Processor loop
public void processOutbox() {
    List<OutboxEntry> pendingMessages = getPendingOutboxEntries(); // Get records with PENDING status

    for (OutboxEntry message : pendingMessages) {
        try {
            sendMessageToClient(message.getPayload(), message.getType()); // Send message to client
            updateOutboxEntryStatus(message.getId(), "SENT", Instant.now());
        } catch (Exception e) {
            log.error("Error sending message", e);
            updateOutboxEntryStatus(message.getId(), "FAILED", null); // Mark as failed or retry
        }
    }
}

Thus, the transactional record in the outbox guarantees that the message will only be sent to the client if the main business transaction completes successfully, ensuring data consistency.