A Developer’s Journey to the Cloud 8: Event-Driven Architecture with Rabbitmq

Arun SDArun SD
5 min read

I Went All-In on Serverless, and It Was a Mistake

The system was a thing of beauty. It was a distributed, resilient, auto-scaling marvel. My infrastructure was code, my application was orchestrated, and my database was a distributed tier that could handle immense traffic. I had climbed every mountain and slain every dragon. The application was, by all technical measures, complete.

But my users didn't care about my beautiful architecture. They cared about how the app felt. And lately, it felt slow in weirdly specific places.

The feedback was consistent:

  • "Signing up took forever."

  • "My profile picture took a long time to appear."

  • "I clicked 'submit order' and the little spinner just spun and spun."

Nothing was crashing. The database wasn't overloaded. The servers weren't stressed. I was staring at a wall of green dashboards, yet the user experience was suffering. My perfectly scaled architecture was still delivering a sluggish experience. It didn't make sense.

The Synchronous Trap

I decided to trace a single "slow" request. A new user signs up. They fill out the form, upload a profile picture, and click "Register." I watched the logs for that single API call:

BEGIN TRANSACTION

INSERT INTO users...

COMMIT

-- User created. Now, process the profile picture...

-- Loading image into memory...

-- Resizing image to 3 different sizes...

-- Uploading 3 thumbnails to cloud storage...

-- Image processing complete. Now, add to mailing list...

-- Calling external MailChimp API...

-- MailChimp API success. Now, send welcome email...

-- Calling external SendGrid API...

-- SendGrid API success. Now, send success response to user.

The whole process took seven seconds. For seven seconds, the user was staring at a loading spinner, waiting for my server to finish its long to-do list. The problem wasn't a bottleneck; it was the process itself. My API was trying to do everything, all at once, in one long, synchronous chain.

The Fork in the Road: Two Paths to Asynchronous

I needed to decouple the initial request from the slow background work. The answer was to shift to an event-driven architecture. My API would just announce that a USER_CREATED event had happened by publishing a message to a queue. Then, something else would listen for that message and do the actual work.

My research led me to two distinct architectural paths:

  1. The Kubernetes-Native Path: Use RabbitMQ as the message queue and a Worker Process as the consumer. Both would run as containers inside my existing Kubernetes cluster. It felt like a natural extension of my current setup.

  2. The Fully Managed, Serverless Path: Go all-in on my cloud provider's ecosystem. Use Amazon SQS (Simple Queue Service) as the message queue and AWS Lambda as the consumer.

My gut told me the Kubernetes path made sense. My app had a constant, steady stream of these jobs, and I was already paying for the Kubernetes cluster, so adding one more small process would be cheap and predictable. But my experience screamed otherwise. Every time I had chosen a managed service RDS for my database, ElastiCache for my cache, it had saved me from anxiety. The lesson seemed obvious: managed is often better.

Ignoring my instinct, I chose the fully managed, serverless path. I configured my API to send a message to an SQS queue, which in turn would trigger my Lambda function. It felt clean. It felt modern. It felt... expensive.

The Wrong Tool

At the end of the month, I got my cloud bill, and my jaw dropped. The cost of running millions of SQS API calls and Lambda invocations was shockingly high. Worse, users were still complaining about occasional slowness. I realized that sometimes, when a user was the first to trigger the function in a while, they were experiencing a "cold start" a multi-second delay while the managed service "woke up" the function. I had even hit the 15-minute execution limit on a large video file, causing a job to fail silently.

My solution had created a new set of problems that were even harder to debug. The managed service wasn't a silver bullet. It was a specific tool for a specific job, and I had chosen it for the wrong one. It was perfect for infrequent, unpredictable tasks, but my workload was constant and predictable.

The Right Tool

Humbled, I deleted the SQS queue and the Lambda function. I went back to my original, instinctual plan.

I deployed RabbitMQ and a simple worker.js process as new containers in my Kubernetes Deployment manifest. The worker's only job was to connect to RabbitMQ and process jobs from the queue.

# deployment.yaml
# ... (existing app container config) ...
      - name: worker-container # <-- The new container
        image: myusername/my-awesome-app-worker:v1.1
        # No ports, it just does background work

I deployed the change. The user registration endpoint was still lightning fast. The background jobs were processed reliably. And my cloud bill went back down to a sane number.

Key Lesson Learned

The real lesson finally sank in, more profound than just "use the right tool." My previous successes had taught me a simple, dangerous rule: "outsource your anxiety." But that rule lacked nuance. The real principle isn't just about outsourcing; it's about understanding what you're trading for what you're getting.

  • With RDS and ElastiCache, I was trading a small amount of money for a massive reduction in operational complexity and a huge gain in reliability. A fantastic trade.

  • With the SQS+Lambda stack, I was trading a lot of money and predictable performance for a small reduction in management overhead. I already had a powerful Kubernetes cluster; adding one more container was practically free and gave me total control.

The journey isn't about blindly following a dogma like "managed is always better." It's about looking at your workload, understanding trade-offs, and leveraging investments you've already made. My system was now a collection of small, independent services communicating through a central message bus. It was fast, resilient, and truly scalable.

But as I looked at the architecture diagram on my screen a web of services, databases, caches, and queues, a new kind of uncertainty settled in. The system was now so complex, so distributed, that I couldn't hold it all in my head anymore. If a user's welcome email never arrived, how would I even begin to debug it? The event was fired, but which worker picked it up? Did it fail? Where are the logs? It was a beautiful, powerful machine, but a complete black box.

up next: A Developer’s Journey to the Cloud 9: Happiness (or So I Thought, Observing the Chaos)

0
Subscribe to my newsletter

Read articles from Arun SD directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Arun SD
Arun SD