When migrating from one cloud queue service to another, rewriting every producer and consumer is impractical. A common approach is to build an abstraction layer that works with both. Here are the hard lessons about where that abstraction holds and where it leaks.
The Abstraction
A clean approach is a CloudQueue interface with implementations for each provider, plus an in-memory implementation for testing. The interface exposes send(), receive(), and acknowledge(). A configuration property determines which implementation is active.
public interface CloudQueue {
void send(String messageBody, Map<String, String> attributes);
List<QueueMessage> receive(int maxMessages, Duration visibilityTimeout);
void acknowledge(QueueMessage message);
}
Where the Abstraction Leaks
Visibility timeouts. Some queue services let you set visibility timeout per-receive-call. Others set it at the queue level during creation. If the abstraction accepts a timeout parameter but the underlying service ignores it, consumers can start reprocessing messages that are still being handled by another instance.
Message attributes. Some services support typed message attributes (String, Number, Binary). Others require putting everything in the message body as JSON. The abstraction has to serialize attributes into the body for the simpler provider and deserialize them on receive, adding latency and a potential deserialization failure point.
Dead letter queues. DLQ configuration varies significantly. Some providers handle it declaratively per-queue. Others require separate DLQ creation and explicit programmatic linking. The abstraction cannot hide this difference cleanly.
The Lesson
Queue abstractions work well for send/receive/ack. They break down for configuration, error handling, and operational semantics. Keep the abstraction for application code but expose cloud-specific configuration for ops.