← All posts

Return to Sender

Today was the fourth day of my schedule, with time split between Python fundamentals and system design.

Python Exercises

I continued working through the Python track on Exercism, covering tuples, dicts, and unpacking.

Tuples

The tuple exercises were all very basic and quick to get through. The one small gotcha was the format of the output when a tuple was interpolated into a string, but that didn’t take long to resolve.

Dicts

Reading through the “iterable views” of a dict (.keys(), .values(), and .items()), I got the sense that they have properties I’ll quickly forget, and that will trip me up later.

I also had another gotcha moment that came from Elixir habits. I tried to update a dict and return it in a single line:

return items.update(new_items)

Just like insert yesterday, update modifies the dict in place and returns None. Without Elixir’s implicit return of a new value from every function, this got me stuck for a few minutes before I split it into two lines:

items.update(new_items)
return items

Unpacking and Multiple Assignment

The last topic covered unpacking collections with *args and **kwargs, including their use as function parameters. It also covered the reverse: packing lists and tuples with *collection, and dicts with **collection.

System Design

Today’s system design topic was message queues. I’ve used a few of them at different companies in the past, including AWS SQS and Kafka, but this was a chance to cover the theory behind them more deliberately.

I went back to the Hello Interview channel I used for database sharding and watched their video on message queues.

When to Use a Queue

A queue is a good fit when:

  • Users don’t need an immediate result: the work can happen asynchronously.
  • Workloads spike: the queue absorbs high volumes at peak traffic.
  • Work needs to scale independently: producers and consumers can be scaled separately.
  • Reliability matters: work isn’t lost if consumers go offline, since the queue holds onto messages until they’re processed and acknowledged.

Delivery Guarantees

There are three delivery guarantees to choose from:

  • At least once: every message is delivered to consumers at least once, but possibly more than once, so consumers need to handle messages idempotently. This is almost always the right choice, being the most practical and safe option.
  • At most once: a message is deleted from the queue as soon as a consumer pulls it. If the consumer fails, the message can’t be retrieved again, so this is only appropriate for low-importance events, like collecting metrics.
  • Exactly once: not very feasible in practice.

Partitioning

To handle increased throughput, a queue can be split into multiple independent partitions, which allows consumers to scale horizontally. The catch is that consumers can only scale as far as the number of partitions.

Messages are assigned to partitions based on a partition key. Using a user ID as the key, for example, guarantees that all messages for the same user land in the same partition, and are consumed in the order they were written.

Even distribution is also an important consideration when choosing a partition key, so that one consumer isn’t doing most of the work while the others sit idle. This sounds a lot like choosing a shard key, and in an interview, it’s worth discussing the trade-offs between ordering and distribution, since a given key may not be the best fit for both.

When Producers Outpace Consumers

When producers out-scale consumers, there are a few options:

  • Autoscaling: most cloud providers support autoscaling consumers.
  • Back pressure: the services producing messages can respond to clients with an error, or a notice of high volume asking them to try again later. This is a strategy that’s worth raising in interviews.
  • Monitoring: alerts on queue depth can raise an alarm when it grows too high.

Dead Letter Queues

A “poisoned message” is a malformed message that can’t be consumed properly, and can potentially crash the consumer. Without any guardrails, it will be retried forever.

Most queues let you specify a maximum retry count. Once a message hits that limit without being processed successfully, it’s written to a separate dead letter queue, where someone can monitor and investigate it.

Durability and Fault Tolerance

To recover from the failure of the queue itself, most queue systems write messages to disk and replicate them across multiple brokers.

  • Kafka: the most widely used. Messages aren’t removed right after they’re consumed, so they can be replayed manually if a consumer fails.
  • SQS: very simple and fully managed, with no infrastructure to worry about. Standard queues offer best-effort ordering with high throughput, while FIFO queues offer strict ordering at the cost of lower throughput.
  • RabbitMQ: less common in system design interviews, but supports complex routing patterns.

Takeaways

The Elixir habit of expecting every function to return a new value tripped me up again today, in almost exactly the same way as yesterday. It’s a good reminder that these assumptions run deep, and that the reps are worth it, even when the exercises feel trivial.

On the system design side, it was nice to put more structure around a tool I’ve already used in practice. The video didn’t go into detail on RabbitMQ, and since I’ve never worked with it myself, I’d like to revisit its routing patterns to learn more about how and when they’re useful.