CQRS and Event Sourcing in Kotlin: Architecture for Flawless Code

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why CQRS and Event Sourcing Are Not Just Buzzwords

\n\n

Imagine your code is a huge museum. Usually, we only store the latest version of an exhibit: "this is what the order looks like now." But what if we want to know how the order ended up in this state? Who pressed the "Send" button? When did the status change? This is where CQRS (Command Query Responsibility Segregation) and Event Sourcing in Kotlin come into play.

\n\n

In this guide, we won't just copy theory from Wikipedia. We'll break down real examples, write code, and understand how these patterns turn the chaos of CRUD operations into a clean architecture. If you're tired of "spaghetti code" and want to build systems that are easy to scale and debug — you've come to the right place.

\n\n

What is CQRS in Simple Terms

\n\n

In a standard architecture, we use the same models for reading and writing data. This is like eating soup and stirring it with the same spoon. CQRS proposes separating responsibilities:

\n\n
    \n
  • Command — changes state. Write requests (Create, Update, Delete).
  • \n
  • Query — reads state. Only SELECT operations.
  • \n
\n\n

Why is this needed? Imagine an online store. When a user places an order, we need to check the warehouse, deduct the item, create a database record, and send an email. This is a complex command. But when a customer simply views their order list — that's a simple query. Mixing them in one method is a sure path to bugs.

\n\n

Benefits of CQRS in Kotlin

\n
    \n
  • Performance: you can optimize reading and writing differently (different databases, caches, indexes).
  • \n
  • Security: separation of access rights (admin writes, user reads).
  • \n
  • Scalability: commands and queries can be distributed across different servers.
  • \n
\n\n

Event Sourcing: Saving History, Not Facts

\n\n

If CQRS separates operations, Event Sourcing changes the way data is stored. Instead of storing the current state (e.g., "balance: 1000 rubles"), we store a sequence of events: "Deposit of 500", "Purchase of 200", "Fee deduction of 50".

\n\n

The current state is simply the result of applying all events in order. This gives a superpower: we can "rewind" time, restore the state for any date, or analyze user behavior.

\n\n

Real-life Example: Bank Account

\n
// Event\ndata class MoneyDeposited(val amount: BigDecimal, val timestamp: Instant)\ndata class MoneyWithdrawn(val amount: BigDecimal, val timestamp: Instant)\n\n// Aggregate (entity)\nclass BankAccount {\n    private var balance: BigDecimal = BigDecimal.ZERO\n    private val events = mutableListOf<Any>()\n\n    fun deposit(amount: BigDecimal) {\n        if (amount <= BigDecimal.ZERO) throw IllegalArgumentException("Amount must be positive")\n        val event = MoneyDeposited(amount, Instant.now())\n        apply(event)\n        events.add(event)\n    }\n\n    fun withdraw(amount: BigDecimal) {\n        if (amount > balance) throw IllegalStateException("Insufficient funds")\n        val event = MoneyWithdrawn(amount, Instant.now())\n        apply(event)\n        events.add(event)\n    }\n\n    private fun apply(event: Any) {\n        when (event) {\n            is MoneyDeposited -> balance += event.amount\n            is MoneyWithdrawn -> balance -= event.amount\n        }\n    }\n\n    fun getBalance(): BigDecimal = balance\n    fun getUncommittedEvents(): List<Any> = events.toList()\n}
\n\n

How CQRS and Event Sourcing Work Together

\n\n

This is a classic tandem. CQRS is responsible for separating channels, and Event Sourcing is responsible for storage

Blogs

Book Recommendations