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.
-
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.
-
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.
-
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.