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
216: Building a Notification Service with Akka Actors
I see this all the time when developers first jump into Akka: they treat Actors as if they are just a fancy way to wrap a Future. They'll build a system where every single interaction is an ask (the ? operator), waiting for a response to come back before proceeding. If you're doing this, you're essentially turning your asynchronous actor system into a synchronous, blocking chain of requests. You've just rebuilt a traditional REST API call inside your own JVM, and you've lost almost all the benefits of the actor model in the process.
The "Ask" Trap: Turning Actors into Blocking Services
Imagine we're building a Notification Service. You might be tempted to write a NotificationManager that does something like this: it receives a request to notify a user, uses ask to send that request to an EmailActor, and then waits for a Future[Success] to return so it can log the result.
// The wrong way: Treating actors like synchronous methods
case class SendEmail(userId: String, message: String)
case class EmailResult(status: String)
// Inside the Manager Actor
def receive = {
case SendEmail(userId, msg) =>
// This is the trap! Waiting for a Future inside an actor
(emailActor ? SendEmail(userId, msg)).map {
case EmailResult(status) => println(s"Sent: $status")
}
}
Here's why this is a disaster: you're tying the lifecycle of your manager to the response time of the email provider. If the email service hangs, you start piling up Futures in memory. You aren't letting the actor do what it's best at—managing state and reacting to events independently. I've seen systems crash under moderate load simply because the developers were "asking" too much and "telling" too little.
The Protocol Approach: Fire-and-Forget with Callbacks
The correct way to think about this is through protocols. Instead of asking "Did this work?", you tell the actor to do the work and tell it where to send the result when it's finished. This keeps the NotificationManager completely non-blocking. It sends a message and immediately moves on to the next notification in the queue.
In a real notification service, we want a dispatcher that delegates to specific channel actors (Email, SMS, Push). Let's look at how we actually structure this using the tell (!) pattern.
sealed trait NotificationMsg
case class Dispatch(userId: String, content: String, channel: Channel) extends NotificationMsg
case class DeliveryReport(userId: String, success: Boolean) extends NotificationMsg
sealed trait Channel
case object Email extends Channel
case object SMS extends Channel
class NotificationManager extends Actor {
val emailActor = context.actorOf(Props[EmailActor], "email-worker")
val smsActor = context.actorOf(Props[SmsActor], "sms-worker")
def receive = {
case Dispatch(userId, content, Email) =>
// Fire and forget. We don't wait.
// We tell the worker to send the report back to 'self'.
emailActor ! SendEmail(userId, content, sender())
case Dispatch(userId, content, SMS) =>
smsActor ! SendSms(userId, content, sender())
case DeliveryReport(userId, success) =>
// The actor reacts to the result whenever it eventually arrives
if (success) println(s"User $userId notified successfully")
else println(s"Failed to notify $userId")
}
}
class EmailActor extends Actor {
def receive = {
case SendEmail(userId, content, replyTo) =>
// Simulate network latency
val success = try {
/* actual email logic */
true
} catch { case _: Exception => false }
replyTo ! DeliveryReport(userId, success)
}
}
Notice the shift. The NotificationManager is now a traffic controller, not a waiter. It doesn't care if the EmailActor takes 10 milliseconds or 10 seconds to respond; it stays responsive to new Dispatch messages. This is the essence of the actor model: decoupling the request from the response.
One detail I want you to catch: passing sender(). In Akka, sender() is a method that returns the ActorRef of whoever sent the current message. By passing this along to the worker actor, we give the worker a way to "call home" without the worker needing to have a hardcoded reference to the manager. It makes your components far more reusable.
📋 Practical Task
Build a Rate-Limited Push Notification Dispatcher
Your goal is to extend the notification service to prevent spamming users. You need to implement a PushNotificationActor that enforces a rate limit: a specific user cannot receive more than 3 push notifications per single "window" of time (for this exercise, just use a simple counter that resets when a ResetLimit message is received).
Requirements:
- Create a
PushNotificationActorthat maintains an internalMap[String, Int]to track the number of messages sent peruserId. - If a user has already reached 3 messages, the actor should not "send" the notification; instead, it should send a
DeliveryReport(userId, success = false)back to the manager immediately. - If the user is under the limit, increment their count and send a
DeliveryReport(userId, success = true). - Implement a
ResetLimitmessage that clears the map, allowing users to receive notifications again. - Integrate this into the
NotificationManagerso thatDispatch(userId, content, Push)routes to this new actor.
There are no comments for now.