Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
155: Event Sourcing for Audit Trails in Scala
I've spent a lot of time reviewing architectures for financial systems, and there is one mistake I see almost every single time someone mentions "audit trails." They tell me, "We've got it covered; we just have an audit_logs table where we insert a row every time a user updates a record."
Audit Tables are not Event Stores
Here is why that approach is fundamentally flawed: it treats the audit trail as a side effect. In a traditional CRUD application, your "Source of Truth" is the current state (the accounts table). The audit log is just a diary of what happened to that state. The problem is that the state and the log can diverge. If a bug in your service updates the balance but fails to write to the audit table, or if a DBA manually tweaks a value in the database to fix a production issue, your audit trail is now a lie.
When we talk about Event Sourcing, we aren't talking about logging what happened. We are talking about making the sequence of events the only source of truth. You don't update a row in a table; you append a fact to a log. If you want to know the current state, you calculate it by replaying those facts.
The Log as the Single Source of Truth
Let's look at this through the lens of a bank account. In a CRUD world, you have a balance column. In an Event Sourced world, you have a stream of events. I prefer this because it's mathematically honest—you don't actually "have" a balance; you have a history of transactions that results in a balance.
case class AccountState(balance: BigDecimal, accountStatus: String)
sealed trait AccountEvent
case class AccountOpened(accountId: String, owner: String) extends AccountEvent
case class MoneyDeposited(amount: BigDecimal) extends AccountEvent
case class MoneyWithdrawn(amount: BigDecimal) extends AccountEvent
case class AccountClosed(reason: String) extends AccountEvent
The magic happens in the "evolve" or "apply" function. This is a pure function that takes the current state and an event, and returns a new state. It's essentially a foldLeft over your event stream.
Implementing the State Fold in Scala
Since we've already covered functional patterns in previous lessons, you'll recognize this as a classic reducer pattern. I usually implement this inside the state object as a method to keep the logic encapsulated.
case class AccountState(balance: BigDecimal = 0, accountStatus: String = "Unknown") {
def applyEvent(event: AccountEvent): AccountState = event match {
case AccountOpened(_, _) => copy(accountStatus = "Open")
case MoneyDeposited(amount) => copy(balance = balance + amount)
case MoneyWithdrawn(amount) => copy(balance = balance - amount)
case AccountClosed(_) => copy(accountStatus = "Closed")
}
}
// To reconstruct the state:
val history = List(
AccountOpened("ACC-123", "Alice"),
MoneyDeposited(100.00),
MoneyWithdrawn(20.00),
MoneyDeposited(50.00)
)
val finalState = history.foldLeft(AccountState()) { (state, event) =>
state.applyEvent(event)
}
// finalState is AccountState(130.0, "Open")
I love this approach because it gives you "time travel" for free. If a client claims their balance was wrong last Tuesday, you don't have to guess based on a vague audit log. You simply replay the events up until last Tuesday and see exactly what the state was. It is the ultimate audit trail because the trail is the data.
Handling the Performance Hit with Snapshots
Now, you're probably thinking: "I can't replay ten thousand events every time a user refreshes their page." You're right; that would be insane. This is where snapshots come in.
A snapshot is simply a cached version of the state at event N. When you need the current state, you load the latest snapshot (say, at event 500) and only replay the events that occurred from 501 onwards. I usually recommend snapshotting every 100 or 1,000 events depending on the event volume. It turns an O(N) operation into an O(1) (or close to it) without sacrificing the integrity of the event log.
📋 Practical Task
Building a Version-Controlled Document Revision System
You are tasked with building a simplified version of a collaborative text editor. Instead of storing the current text of a document, you must implement an Event Sourced system that reconstructs the document from a series of edits.
Requirements:
- Define a sealed trait
DocEventwith three types of events:TextAdded(position: Int, text: String)TextDeleted(position: Int, length: Int)TitleChanged(newTitle: String)
- Create a
DocumentStatecase class that holds the currenttitle: Stringand thecontent: String. - Implement a method
applyEvent(event: DocEvent): DocumentStateinsideDocumentState.- Hint: Use
StringBuilderor string slicing to handle thepositionandlengthfor text edits.
- Hint: Use
- Write a test case that takes a
List[DocEvent]and usesfoldLeftto produce the finalDocumentState.
Verification: If you start with an empty document and apply:
TitleChanged("Notes"), TextAdded(0, "Hello "), TextAdded(6, "World"), TextDeleted(0, 1), your final state should have the title "Notes" and the content "ello World".
There are no comments for now.