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
208: Schema Registry Integration with Kafka
If you've spent any time in production with Kafka, you know the "poison pill" scenario: someone changes a field from an Integer to a String in the producer, and suddenly every single consumer in your pipeline starts crashing with a serialization error. It's a nightmare to clean up. That's why we use a Schema Registry. It acts as a central source of truth, ensuring that producers and consumers are speaking the same language without having to hardcode schemas into every single microservice.
For this lesson, we're going to build a simple event producer and consumer for a UserActivity stream. We'll use Apache Avro, as it's the gold standard for this workflow.
Defining our UserActivity schema
First, we don't write Scala classes for our data; we write an Avro schema file. This allows the Registry to version the schema. I'm creating a file called user_activity.avsc. I want to track the user's ID, the action they took, and a timestamp.
{
"type": "record",
"name": "UserActivity",
"namespace": "com.example.events",
"fields": [
{"name": "userId", "type": "string"},
{"name": "action", "type": "string"},
{"name": "timestamp", "type": "long"}
]
}
Once this is defined, I use a plugin like sbt-avrohugger to generate the Scala case classes from this JSON. This gives us a type-safe UserActivity object to work with in our code, while the Registry handles the actual bytes on the wire.
Configuring the Producer for Avro
Now, let's get the producer running. The key difference here is that we can't use the standard StringSerializer or ByteArraySerializer. We need the KafkaAvroSerializer provided by Confluent.
val props = new Properties()
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer].getName)
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[KafkaAvroSerializer].getName)
props.put("schema.registry.url", "http://localhost:8081")
val producer = new KafkaProducer[String, UserActivity](props)
val activity = UserActivity.newBuilder()
.setUserId("user-123")
.setAction("click_signup")
.setTimestamp(System.currentTimeMillis())
.build()
producer.send(new ProducerRecord("user-activities", "user-123", activity))
Correcting my serialization slip-up
I'll be honest: when I first wrote this snippet a few minutes ago, I forgot to add the schema.registry.url property. I hit run and immediately got a KafkaException: Error constructing producer because the KafkaAvroSerializer had no idea where to register the schema. It doesn't default to a local port; it needs an explicit URL.
If you see an error mentioning that the serializer cannot find the registry configuration, don't waste time debugging your Kafka brokers. Check your Properties object. The serializer needs that URL to check if the schema already exists or if it needs to register a new version.
Wiring up the Consumer to respect the schema
On the consumer side, we do the opposite. We use the KafkaAvroDeserializer. One detail I always emphasize is the specific.avro.reader property. If you set this to false, Kafka returns a GenericRecord (which feels like a Map). If you set it to true, it maps the data back into our generated UserActivity Scala class.
val consumerProps = new Properties()
consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "activity-monitor")
consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer].getName)
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[KafkaAvroDeserializer].getName)
consumerProps.put("schema.registry.url", "http://localhost:8081")
consumerProps.put("specific.avro.reader", "true")
val consumer = new KafkaConsumer[String, UserActivity](consumerProps)
consumer.subscribe(java.util.Collections.singletonList("user-activities"))
while (true) {
val records = consumer.poll(java.time.Duration.ofMillis(100))
records.forEach { record =>
println(s"User ${record.value().getUserId} performed ${record.value().getAction}")
}
}
By using the Registry, the consumer doesn't need the .avsc file bundled in its source code. It just asks the Registry: "I see schema ID 5 in this message header; can you give me the definition for that?" and the Registry handles the rest.
📋 Practical Task
Exercise: Implementing Schema Evolution for UserActivity
In a real-world scenario, requirements change. Your task is to evolve the UserActivity schema without breaking the existing consumer.
- Modify the Schema: Update
user_activity.avscto add a new field calleddeviceType(a string). To ensure backward compatibility, you must provide a default value of"unknown". - Update the Producer: Update your Scala producer to populate this new
deviceTypefield (e.g., "mobile" or "desktop"). - Verify Compatibility: Run the original consumer (the one without the
deviceTypefield in its generated classes) and verify that it can still read the new messages produced by the updated producer without crashing. - Update the Consumer: Finally, regenerate the Scala classes for the consumer and update the print statement to include the
deviceType.
There are no comments for now.