← Back to list

Designing Event-Driven Architecture in Ruby on Rails with Kafka

Building scalable, resilient, and loosely coupled Rails applications using Apache Kafka.

Ravi Prakash · 2026-07-16 07:51 · 0 claps · 4.2 min read paywalled
#ruby #ruby-on-rails #ruby-on-rails-development #rails #software-development
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🔧 · Data Engineering 🏛️ · Architecture

Designing Event-Driven Architecture in Ruby on Rails with Kafka

Building scalable, resilient, and loosely coupled Rails applications using Apache Kafka.

Introduction

As applications grow, the traditional request-response architecture starts showing its limitations. Features such as sending emails, updating analytics, processing payments, syncing third-party services, and generating reports often become tightly coupled within a single request.

This leads to:

  • Slow API responses
  • Tightly coupled services
  • Difficult deployments
  • Poor scalability
  • Reduced fault tolerance

This is where Event-Driven Architecture (EDA) shines.

Instead of directly calling another service, your application publishes an event. Any interested service can subscribe to that event and react independently.

In this article, we’ll build a production-ready Event-Driven Architecture using Ruby on Rails and Apache Kafka, exploring real-world patterns, implementation techniques, and best practices.

What is Event-Driven Architecture?

Event-Driven Architecture is a software design pattern where components communicate by producing and consuming events rather than invoking each other directly.

Imagine an e-commerce application.

When an order is placed:

Customer
    │
    ▼
Rails API
    │
    ▼
Order Created

Instead of calling multiple services synchronously:

Order Service
    │
    ├── Email Service
    ├── Inventory Service
    ├── Analytics Service
    ├── Loyalty Service
    └── Notification Service

The Order Service simply publishes:

order.created

Then Kafka distributes the event to every interested consumer.

Kafka
order.created
     │
     ├── Email Consumer
     ├── Inventory Consumer
     ├── Analytics Consumer
     ├── Notification Consumer
     └── Billing Consumer

Each consumer works independently.

Why Kafka?

Apache Kafka is one of the most popular distributed event streaming platforms.

Benefits include:

  • High throughput
  • Fault tolerance
  • Event persistence
  • Horizontal scalability
  • Ordered events
  • Consumer groups
  • Replay capability

It’s widely used by companies like LinkedIn, Uber, Netflix, Airbnb, and many large-scale SaaS platforms.

System Architecture

+------------------+
                | Ruby on Rails API|
                +------------------+
                        |
                        |
                Publish Events
                        |
                        ▼
                +------------------+
                |      Kafka       |
                +------------------+
                 |      |      |
        ----------      |      ----------
       ▼                ▼               ▼
Inventory       Email Service     Analytics
 Payment         Notification      CRM
 Search          Reporting         Billing

Setting Up Kafka

The easiest way to start locally is using Docker Compose.

version: '3'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
  kafka:
    image: confluentinc/cp-kafka
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092

Start Kafka.

docker compose up

Installing ruby-kafka

Add the gem.

gem 'ruby-kafka'

Install.

bundle install

Configuring Kafka

Create:

config/initializers/kafka.rb
KAFKA = Kafka.new(
  ["localhost:9092"],
  client_id: "rails_app"
)

Now Kafka is available globally.

Creating an Event Publisher

Instead of publishing events everywhere, create a reusable publisher.

app/services/event_publisher.rb
class EventPublisher
  def self.publish(topic, payload)
    producer = KAFKA.async_producer
    producer.produce(
      payload.to_json,
      topic: topic
    )
    producer.deliver_messages
  end
end  

Simple.

Reusable.

Testable.

Publishing Events

Suppose a customer places an order.

class OrdersController < ApplicationController
  def create
    order = Order.create!(order_params)
    EventPublisher.publish(
      "order.created",
      {
        order_id: order.id,
        customer_id: order.customer_id,
        total: order.total
      }
    )
    render json: order
  end
end

Notice the controller doesn’t know who will consume the event.

This is loose coupling.

Building a Consumer

Consumers subscribe to topics.

consumer = KAFKA.consumer(
  group_id: "inventory-service"
)
consumer.subscribe("order.created")

Process messages.

consumer.each_message do |message|
data = JSON.parse(message.value)
  Inventory.reserve_stock(
    data["order_id"]
  )
end

Inventory is completely independent from Orders.

Creating Multiple Consumers

The same event can power multiple systems.

order.created
│
        │
        ▼
Inventory
Email
Analytics
CRM
Notification
Fraud Detection
Billing

None of these services know about each other.

Event Versioning

Events evolve over time.

Bad practice:

{
  "order_id": 12
}

Better:

{
  "version": 2,
  "event": "order.created",
  "data": {
    "order_id": 12,
    "customer_id": 55,
    "total": 399
  }
}

Versioning prevents breaking older consumers.

Event Naming Convention

A good naming strategy is:

entity.action

Examples:

user.created
user.deleted
payment.completed
payment.failed
order.shipped
invoice.generated

Keep names meaningful and immutable.

Handling Failures

Consumers should never crash because of one bad event.

Wrap processing.

consumer.each_message do |message|
begin
    process(message)
  rescue => e
    Rails.logger.error(e)
  end
end

Production systems should also use:

  • Dead Letter Queues (DLQ)
  • Retry mechanisms
  • Alerting
  • Monitoring

Idempotency

Consumers may receive duplicate events.

Never assume Kafka delivers exactly once.

Bad:

Order.update(status: "paid")

Better:

return if Payment.exists?(event_id: event_id)
Payment.create!(...)

Always make event processing idempotent.

Partitioning

Kafka distributes messages into partitions.

Orders
Partition 1
Partition 2
Partition 3
Partition 4

Messages sharing the same key remain ordered.

producer.produce(
  payload,
  topic: "orders",
  partition_key: order.id
)

Ordering is preserved for each order.

Consumer Groups

Consumer groups enable horizontal scaling.

Inventory Group
Consumer A
Consumer B
Consumer C

Kafka automatically balances partitions across consumers.

This allows processing millions of events efficiently.

Retry Strategy

Avoid immediate retries.

Instead:

Main Topic
↓
Retry Topic
↓
Retry Topic 2
↓
Dead Letter Queue

This prevents endless processing loops and gives operators a safe place to inspect failed events.

Monitoring Kafka

Track key metrics:

  • Consumer lag
  • Failed messages
  • Processing time
  • Throughput
  • Retry count
  • Topic size
  • Broker health

Tools commonly used:

  • Datadog
  • Prometheus
  • Grafana
  • Kafka UI

Monitoring is essential for production systems.

Security Best Practices

Always secure Kafka clusters.

Recommended practices:

  • TLS encryption
  • SASL authentication
  • ACL authorization
  • Separate topics by domain
  • Encrypt sensitive payloads
  • Never publish passwords or tokens

Events should contain only the data required by consumers.

Testing Event Publishing

RSpec makes event testing straightforward.

describe EventPublisher do

  it "publishes an event" do
    producer = instance_double("Kafka::AsyncProducer")
    allow(KAFKA).to receive(:async_producer)
      .and_return(producer)
    expect(producer)
      .to receive(:produce)
    EventPublisher.publish(
      "orders",
      { id: 1 }
    )
  end
end

This keeps tests fast and isolated from Kafka.

Common Mistakes

Avoid these pitfalls:

  • Publishing entire ActiveRecord objects
  • Creating too many topics
  • Ignoring schema evolution
  • Missing idempotency
  • Blocking consumers with slow operations
  • Forgetting retries
  • Not monitoring consumer lag
  • Treating Kafka as a request-response system

Production Tips

  • Keep events small and focused.
  • Use meaningful topic names.
  • Version event payloads.
  • Design consumers to be idempotent.
  • Separate producers and consumers into dedicated services when scaling.
  • Monitor lag, retries, and throughput continuously.
  • Document your event contracts and treat them as public APIs.

Conclusion

Event-Driven Architecture transforms the way Rails applications scale. By introducing Kafka as an event backbone, you decouple services, improve resilience, and enable independent evolution of business capabilities.

Whether you’re processing orders, sending notifications, synchronizing external systems, or analyzing user behavior, Kafka provides a robust foundation for asynchronous communication. Combined with Ruby on Rails, it allows teams to build systems that are easier to maintain, simpler to scale, and better prepared for the demands of modern distributed applications.

As your application grows, adopting event-driven patterns isn’t just an optimization — it’s an architectural investment that pays dividends in flexibility, reliability, and long-term scalability.

Happy coding!


메타데이터
post_id
e0be2a941e63
slug
designing-event-driven-architecture-in-ruby-on-rails-with-kafka-e0be2a941e63
url
https://medium.com/@raviskit2012/designing-event-driven-architecture-in-ruby-on-rails-with-kafka-e0be2a941e63
canonical_url
https://medium.com/@raviskit2012/designing-event-driven-architecture-in-ruby-on-rails-with-kafka-e0be2a941e63
author_url
https://medium.com/@raviskit2012
status
ok
fetched_at
2026-07-17 05:45:21