The World is Asynchronous and Event-Driven
- 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()
}
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:
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:
- Flexibility: New features can connect to existing events without modifying legacy code
- Scalability: Components can be scaled independently
- Resilience: Failures in one component don't bring down the entire system
- Auditing: Complete history of everything that happened
- 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."