The World is Asynchronous and Event-Driven

Key Points
  • Synchronous systems don't reflect how the real world works - nobody waits on the line while long processes happen, but our systems often do exactly that
  • Event Sourcing stores the entire sequence of events instead of just the current state, allowing you to reconstruct any moment from the past and create multiple projections from the same data
  • CQRS separates write operations from read operations, optimizing each side independently and enabling selective scalability based on actual usage
  • Sagas coordinate distributed processes across multiple services with automatic compensation capabilities when something fails midway
  • Companies adopting event-driven architecture gain competitive advantages in flexibility, scalability, resilience, and personalization capabilities based on complete history

It's Monday morning. You drop off your phone at the repair shop and head to work. Three hours later, you receive a message: "Your device is ready for pickup." Simple, right? You didn't wait at the shop for three hours. You didn't call every 10 minutes asking if it was ready. You were simply... notified when the relevant event happened.

This is the essence of an event-driven world — and it's exactly how most real-world processes work. So why do we insist on building systems that work as if you had to stand in line at the repair shop?

The Inconvenient Reality of Synchronous Systems

Imagine if the world worked synchronously:

  • You call a restaurant to order food and stay on the line for 45 minutes waiting for the dish to be ready
  • You go to the doctor and need to wait in the exam room until all test results are ready
  • You send an email and your program freezes until the person responds

Sounds ridiculous, doesn't it? But that's exactly how most of our systems work — making synchronous, blocking requests, waiting for responses that may never come or take longer than expected.

What Are Events

An event is a significant occurrence, a relevant fact that can be considered worthy of attention. It's something that happened in the past and has an impact on our system's state.

Think of it like frames in a movie. A single frame (the current state) tells you very little about the story. But a sequence of frames (events) tells a complete narrative:

// Current state - limited information
data class User(
    val id: String,
    val name: String,
    val plan: String = "premium"
)

// Sequence of events - complete story
sealed class UserEvent {
    data class UserCreated(val id: String, val name: String) : UserEvent()
    data class PlanChanged(val id: String, val previousPlan: String, val newPlan: String) : UserEvent()
    data class LoginPerformed(val id: String, val timestamp: Long) : UserEvent()
    data class PurchaseMade(val id: String, val amount: Double, val product: String) : UserEvent()
}

The current state tells you the user has a premium plan. The events tell you they started on the basic plan, upgraded after two weeks, made three purchases last month, and have a login pattern on Monday mornings.

Building Systems That Reflect Reality

Event Sourcing

Instead of storing just the current state, event sourcing stores all events that led to that state:

class BankAccount(private val events: MutableList<AccountEvent> = mutableListOf()) {
    
    fun deposit(amount: Double) {
        val event = DepositMade(amount, System.currentTimeMillis())
        events.add(event)
        // Publish the event to other interested systems
        eventBus.publish(event)
    }
    
    fun withdraw(amount: Double) {
        require(currentBalance() >= amount) { "Insufficient balance" }
        val event = WithdrawalMade(amount, System.currentTimeMillis())
        events.add(event)
        eventBus.publish(event)
    }
    
    fun currentBalance(): Double {
        return events.filterIsInstance<DepositMade>().sumOf { it.amount } -
               events.filterIsInstance<WithdrawalMade>().sumOf { it.amount }
    }
    
    fun history(): List<AccountEvent> = events.toList()
}
Competitive Advantage
With event sourcing, you can reconstruct the state at any point in the past, perform complete auditing of changes, and create different projections of the same data for different purposes.

Asynchronous Processing

Here's where the magic really happens. Instead of processing everything immediately, you react to events as they happen:

class EventProcessor {
    
    fun process(event: UserEvent) {
        when (event) {
            is UserCreated -> {
                // Processes that can be asynchronous
                sendWelcomeEmail(event.id)
                createRecommendationProfile(event.id)
                addMetrics("user_created")
            }
            
            is PurchaseMade -> {
                // Multiple systems can react to the same event
                updateInventory(event.product)
                calculateSalesCommission(event.amount)
                triggerRecommendations(event.id)
                updateAnalytics(event)
            }
        }
    }
    
    private suspend fun sendWelcomeEmail(userId: String) {
        // Asynchronous operation that doesn't block the main flow
        coroutineScope.launch {
            val template = fetchTemplate("welcome")
            val user = fetchUser(userId)
            emailService.send(user.email, template)
        }
    }
}

Message Brokers

For larger systems, you need dedicated infrastructure to manage events:

class EventBus {
    private val subscribers = mutableMapOf<String, MutableList<(Any) -> Unit>>()
    
    inline fun <reified T> subscribe(crossinline handler: (T) -> Unit) {
        val eventType = T::class.simpleName!!
        subscribers.getOrPut(eventType) { mutableListOf() }
            .add { event -> if (event is T) handler(event) }
    }
    
    fun publish(event: Any) {
        val eventType = event::class.simpleName!!
        subscribers[eventType]?.forEach { handler ->
            // Asynchronous processing to not block the publisher
            coroutineScope.launch {
                try {
                    handler(event)
                } catch (e: Exception) {
                    // Log and error recovery without failing the entire system
                    logger.error("Error processing event $eventType", e)
                }
            }
        }
    }
}

Advanced Patterns

CQRS

Separate write operations (commands) from read operations (queries):

// Command Side - optimized for writes
class OrderCommandHandler {
    fun createOrder(command: CreateOrder): Result<String> {
        return try {
            val event = OrderCreated(
                id = UUID.randomUUID().toString(),
                userId = command.userId,
                items = command.items,
                timestamp = System.currentTimeMillis()
            )
            
            eventStore.append(event)
            eventBus.publish(event)
            Result.success(event.id)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Query Side - optimized for reads
class OrderQueryHandler {
    private val projections = mutableMapOf<String, OrderProjection>()
    
    init {
        // Listen to events and build projections optimized for queries
        eventBus.subscribe<OrderCreated> { event ->
            projections[event.id] = OrderProjection(
                id = event.id,
                user = fetchUserName(event.userId),
                totalAmount = event.items.sumOf { it.price },
                status = "created"
            )
        }
    }
    
    fun findOrder(id: String): OrderProjection? = projections[id]
}

Sagas

For processes involving multiple services:

class OrderProcessingSaga {
    
    suspend fun process(orderCreated: OrderCreated) {
        try {
            // Step 1: Reserve inventory
            val inventoryReserved = inventoryService.reserve(orderCreated.items)
            
            // Step 2: Process payment
            val paymentProcessed = paymentService.process(
                orderCreated.userId, 
                orderCreated.totalAmount
            )
            
            // Step 3: Confirm order
            eventBus.publish(OrderConfirmed(orderCreated.id))
            
        } catch (e: Exception) {
            // Compensation: undo already performed operations
            compensate(orderCreated.id)
            eventBus.publish(OrderFailed(orderCreated.id, e.message))
        }
    }
    
    private suspend fun compensate(orderId: String) {
        // Reverse inventory reservation, cancel payment, etc.
        inventoryService.cancelReservation(orderId)
        paymentService.cancel(orderId)
    }
}

The Power of Personalization and Context

With event-driven architecture, you can build systems that truly understand your users:

Real Example
Imagine an e-commerce site that knows you always buy cat food in the first week of the month. Instead of bombarding you with random offers, it can send you a subtle notification when you're running low on food, based on your purchase history.

This is only possible because the system maintains the complete history of events, not just the current state of your cart.

Practical Implementation

You don't need to refactor your entire system at once. Start by identifying a specific flow:

// Start with something simple like notifications
class NotificationService {
    
    init {
        eventBus.subscribe<UserLoggedIn> { event ->
            // If the user hasn't logged in for more than 7 days
            if (daysSinceLastLogin(event.userId) > 7) {
                sendNotification(event.userId, "We miss you!")
            }
        }
        
        eventBus.subscribe<PurchaseMade> { event ->
            // Personalization based on history
            val recommendations = generateRecommendations(event.userId, event.product)
            sendEmail(event.userId, "Similar products", recommendations)
        }
    }
}

Why Invest Now?

Companies investing in event-driven architecture today have a significant competitive advantage:

  1. Flexibility: New features can connect to existing events without modifying legacy code
  2. Scalability: Components can be scaled independently
  3. Resilience: Failures in one component don't bring down the entire system
  4. Auditing: Complete history of everything that happened
  5. Intelligence: Data for machine learning and personalization

Tools and Technologies

To implement event-driven architecture, consider:

  • Message Brokers: Apache Kafka, RabbitMQ, AWS SQS
  • Event Stores: EventStore, Apache Kafka, AWS DynamoDB
  • Stream Processing: Apache Kafka Streams, Apache Flink
  • Libraries: Axon Framework (Java), EventFlow (.NET), Commanded (Elixir)

Conclusion

The real world is asynchronous. People don't stand still waiting for things to happen — they react when relevant events occur. Our systems should work the same way.

It's not enough to have just the "photo" of the current state. You need the complete "movie" — the sequence of events that tells the story of how you got here. That story is where the real value lies: in the ability to understand patterns, predict behaviors, and create truly personalized experiences.

The question isn't if you should adopt event-driven architecture, but when. And for many companies, that moment is now — before their competitors discover the competitive advantage this approach offers.

As a wise developer would say: "The future belongs to those who can see not just where they are, but also the path they took to get there."