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
107: Working with Kafka from Scala
A few years ago, I was mentoring a developer who was tasked with building a real-time order processing system. Everything worked perfectly in staging, but the moment they hit production, the service entered a death spiral. A single malformed JSON message—what we call a "poison pill"—had entered the Kafka topic. The consumer would read the message, throw a `JsonParseException`, crash, restart, and immediately read the exact same message again. It happened every few seconds for three hours until we manually skipped the offset. It was a baptism by fire in the reality of distributed streaming: your code doesn't just need to handle the data; it needs to handle the failure of the data.
Bridging Java Clients with Scala Types
Now, here is the thing about Kafka: the gold standard is still the official Java client. While there are fantastic wrappers like FS2-Kafka or Alpakka, you need to understand the underlying Java mechanics to debug them. When you bring these into Scala, you're dealing with a lot of mutable Java collections and Properties objects. I always recommend wrapping the producer in a Scala class to hide that ugliness from the rest of your business logic.
To send a message, you'll use the KafkaProducer. The key is to remember that send() is asynchronous. It returns a Future (the Java version), which doesn't play nicely with Scala's scala.concurrent.Future. If you're just firing and forgetting, it's easy, but if you need a guarantee that the message hit the broker, you'll want to use a callback or call .get()—though be careful with the latter, as it blocks your thread.
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord, ProducerConfig}
import java.util.Properties
class OrderEventProducer(brokers: String) {
val props = new Properties()
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer")
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer")
val producer = new KafkaProducer[String, String](props)
def publishOrder(orderId: String, payload: String): Unit = {
val record = new ProducerRecord("orders-topic", orderId, payload)
producer.send(record, (metadata, exception) => {
if (exception != null) println(s"Failed to send order $orderId: ${exception.getMessage}")
})
}
}
The Consumer Loop and Offset Management
Consuming is where things get tricky. Unlike a REST API where the request comes to you, in Kafka, you are the one asking for data. You have to implement a poll loop. This is where most developers stumble—they forget that the poll() method is the heartbeat of the consumer. If you spend too much time processing a single batch of messages without calling poll() again, Kafka will think your consumer has died and trigger a "rebalance," kicking you out of the group and assigning your partitions to someone else.
I usually suggest processing your records in a way that allows you to commit offsets explicitly. If you rely on enable.auto.commit = true, you might commit an offset for a message you haven't actually finished processing, leading to data loss if the app crashes. By disabling auto-commit, you tell Kafka, "I've actually handled this; now you can move the pointer forward."
Handling the Poison Pill
To avoid the crash loop I mentioned earlier, you must wrap your processing logic in a try-catch block inside the loop. If a message is unparseable, don't let the exception bubble up to the main loop. Instead, log the error, perhaps send the bad message to a "Dead Letter Queue" (DLQ) topic for manual inspection, and move on to the next record.
In Scala, we can make this elegant by treating the batch of records as a collection and using Either to represent success or failure. This keeps your pipeline clean and ensures that one bad piece of JSON doesn't bring down your entire infrastructure.
import org.apache.kafka.clients.consumer.{KafkaConsumer, ConsumerConfig, ConsumerRecords}
import java.time.Duration
val consumer = new KafkaConsumer[String, String](consumerProps)
consumer.subscribe(util.Arrays.asList("orders-topic"))
while (true) {
val records: ConsumerRecords[String, String] = consumer.poll(Duration.ofMillis(100))
records.forEach { record =>
try {
processOrder(record.value())
} catch {
case e: Exception =>
println(s"Skipping poison pill at offset ${record.offset()}: ${e.getMessage}")
// sendToDeadLetterQueue(record)
}
}
consumer.commitSync()
}
📋 Practical Task
Build a Resilient Order Event Processor
Your task is to implement a Kafka consumer in Scala that can handle a stream of "Order" strings. Some of these strings will be valid JSON (e.g., {"id": "123", "amount": 50.0}), and some will be intentionally malformed (e.g., "INVALID_DATA").
Requirements:
- Create a
OrderProcessorclass that initializes aKafkaConsumer. - Implement a
run()method containing awhile(true)loop that polls theorders-topic. - Inside the loop, process each record. If the record is a valid JSON string, print "Processed order: [id]". If it is malformed, catch the exception, print "Error: Malformed message at offset [offset]", and ensure the loop continues to the next message without crashing.
- Disable
ENABLE_AUTO_COMMIT_CONFIGand callcommitSync()only after the entire batch of records from a single poll has been processed. - Ensure you use a
Durationof 100ms for the poll timeout.
There are no comments for now.