Skip to content

Queue with a dead-letter queue

The first multi-resource pattern: two resources that always travel together, with the redrive policy wired between them. It returns a small handle (QueueWithDlq) holding the logical IDs, ready-made references, and the resources to put in the template.

@dataclass(frozen=True)
class QueueWithDlq:
    """The handle `queue_with_dlq` returns: the logical IDs, how to refer to them, and the resources."""

    queue: str
    dlq: str
    resources: Resources

    @property
    def queue_arn(self) -> Expr[str]:
        return get_att(self.queue, "Arn")

    @property
    def queue_url(self) -> Expr[str]:
        return ref(self.queue)

    @property
    def dlq_url(self) -> Expr[str]:
        return ref(self.dlq)


def queue_with_dlq(
    name: str,
    *,
    visibility_timeout: int = 30,
    max_receive_count: int = 3,
    tags: list[Expr[Tag]] | None = None,
) -> QueueWithDlq:
    """A queue and its dead-letter queue, with logical IDs `{name}Queue` and `{name}DLQ`."""
    queue_id, dlq_id = f"{name}Queue", f"{name}DLQ"
    dlq: sqs.QueueProps = {}
    main: sqs.QueueProps = {
        "VisibilityTimeout": visibility_timeout,
        "RedrivePolicy": {"deadLetterTargetArn": get_att(dlq_id, "Arn"), "maxReceiveCount": max_receive_count},
    }
    if tags:
        dlq["Tags"] = main["Tags"] = tags
    return QueueWithDlq(queue_id, dlq_id, {dlq_id: sqs.queue(**dlq), queue_id: sqs.queue(**main)})
orders = queue_with_dlq("Order", visibility_timeout=180, tags=tags(Stage="prod"))
orders.queue_arn   # GetAtt OrderQueue.Arn
orders.queue_url   # Ref OrderQueue

orders.resources renders as:

{
  "OrderDLQ":   { "Type": "AWS::SQS::Queue", "Properties": { "Tags": [ ... ] } },
  "OrderQueue": { "Type": "AWS::SQS::Queue", "Properties": {
      "VisibilityTimeout": 180,
      "RedrivePolicy": {
        "deadLetterTargetArn": { "Fn::GetAtt": ["OrderDLQ", "Arn"] },
        "maxReceiveCount": 3
      },
      "Tags": [ ... ] } }
}

Things worth noticing

  • The handle is the contract. The caller never rebuilds "OrderQueue" by hand: orders.queue_arn and orders.queue_url come from the same IDs the pattern used for the GetAtt, so the two can never disagree, and a typo is an attribute error.
  • Typed property dicts, not **kwargs. dlq and main are annotated sqs.QueueProps, so each key and value is checked, and the optional Tags key is only added when there are tags. Unpacking them with ** into the factory keeps the output free of empty lists.
  • The tags use one list for both queues, which is fine because the list is not mutated afterwards. If you ever do mutate shared data, copy it first.