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
108: Schema Evolution with Avro in Scala
Listen, the biggest headache in distributed systems isn't usually the logic—it's the data changing under your feet. You deploy a new version of a service, it starts writing data to Kafka using a new schema, and suddenly your downstream consumers start crashing because they don't know what to do with the new fields. That's where Avro's schema evolution comes in.
Avro is designed specifically to handle this "version drift." The magic happens because Avro requires both the writer's schema (what the data was written with) and the reader's schema (what the current code expects). If they differ, Avro tries to resolve them using a set of predefined rules.
Defining our first UserProfile
Let's build a simple user profile system. I'm using avro4s here because writing raw JSON schema files by hand is a recipe for typos and misery. In Scala, we can just use case classes.
import avro4s._ import avro4s.AvroSchema case class UserProfileV1(id: Long, username: String) val schemaV1 = AvroSchema[UserProfileV1] // This generates a schema with two required fields: id and usernameAt this point, everything is simple. We write a
UserProfileV1, we read it back. No drama.Adding an optional email without breaking the world
Now, imagine the business decides we need to capture user emails. If I just add a field, any old data sitting in my logs or Kafka topics won't have an email. If the reader expects one and doesn't find it, the whole process blows up.
To evolve this safely, we need a default value. This is the golden rule of Avro evolution: if you add a field, you must give it a default so the reader knows what to plug in when processing old records.
case class UserProfileV2( id: Long, username: String, email: Option[String] = None // The Option + default value is key here ) val schemaV2 = AvroSchema[UserProfileV2]Because I used
Optionand providedNone, Avro marks this field as having a default ofnullin the schema. WhenUserProfileV2reads aV1record, it sees the email is missing and simply fills it withNone. This is backward compatible.The "Oops" moment: The required field trap
Here is where I usually trip up when I'm rushing a feature. Let's say I want to add a
accountTierfield (like "Bronze", "Silver", "Gold"). I decide that every user must have a tier, so I add it as a plain String.// I'm making a mistake here... case class UserProfileV3( id: Long, username: String, email: Option[String] = None, accountTier: String // Oops! No default value. )I deploy this. The new code starts reading old
V2data. Suddenly, I get aAvroTypeException: "Found UserProfileV2, expecting UserProfileV3. Field accountTier is missing."I forgot that "required" in the reader's schema means "must exist in the writer's schema." If the old data doesn't have it, and there's no default to fall back on, Avro has no choice but to fail. I've just broken my production pipeline.
Fixing the evolution with a sensible default
To fix this, I have to ensure that any new field added to the schema is either optional or has a fallback value. If
accountTieris mandatory for the business logic, I should still provide a "safe" default for legacy data.case class UserProfileV3Fixed( id: Long, username: String, email: Option[String] = None, accountTier: String = "Bronze" // Now it's safe! )Now, when the
V3Fixedreader encounters aV1orV2record, it says: "I don't see an accountTier here, but the schema tells me to use 'Bronze' if it's missing." Everything flows smoothly again.Just remember: if you're evolving a schema in a production environment, never add a field without a default, and never remove a field that didn't have a default (because old readers won't know how to handle its absence). Keep it simple, keep it optional, or keep it defaulted.
📋 Practical Task
Exercise: Implementing a Compatible Version 3 for OrderEvents
You are maintaining an event-driven system for an e-commerce store. You currently have the following case class representing an order event:
case class OrderEvent(orderId: String, totalAmount: Double)
Your task is to evolve this schema to version 2 and version 3 while ensuring backward compatibility (meaning the newest code can read the oldest data).
- Requirement 1: Create
OrderEventV2. Add a fieldcurrency(String). Since old orders don't have this, it must default to"USD". - Requirement 2: Create
OrderEventV3. Add a fielddiscountCode. This field should be optional (useOption[String]) and default toNone.
Write the Scala case classes for OrderEventV2 and OrderEventV3. Ensure that both follow the rules of Avro schema evolution so that a V3 reader can successfully process a V1 record without throwing an exception.
There are no comments for now.