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
149: Building Resilient Systems with Akka Supervision
A few years ago, I was mentoring a developer who was building a high-throughput payment gateway. He had wrapped almost every single line of his Actor's logic in massive try-catch blocks. He was terrified that if a single network timeout occurred, the Actor would die and the system would lose track of the transaction. The irony was that by trying to prevent the crash, he created a "zombie" actor. When an exception was caught and silenced, the actor's internal state became inconsistent, and it spent the next three hours processing payments with corrupted data. It was a nightmare to debug because the logs showed no errors, but the database was a mess.
That's the classic mistake when moving from traditional OOP to Akka. In a standard app, you catch exceptions to keep the thread alive. In Akka, we embrace the "Let it crash" philosophy. We don't try to anticipate every possible failure inside the actor itself; instead, we delegate the recovery logic to the actor's parent. This is the essence of supervision.
The Hierarchy of Failure
In Akka, every actor is created by another actor. This creates a parent-child hierarchy that mirrors a corporate org chart. If a child actor throws an exception, it doesn't just vanish into the void; it suspends itself and sends a failure signal to its parent. The parent then decides the fate of the child based on a defined SupervisorStrategy.
You have four primary directives to choose from when a child fails:
- Resume: The parent tells the child to just keep going. The actor keeps its current state. This is useful for minor, transient errors that don't corrupt the internal state.
- Restart: This is the most common choice. The actor is killed and a fresh instance is started. The internal state is wiped, but the actor keeps its original identity (mailbox and name). It's the software equivalent of "turning it off and on again."
- Stop: The actor is terminated permanently. Use this if the failure is fatal and the actor can't possibly recover.
- Escalate: The parent says, "I don't know how to handle this," and passes the failure up to its own parent.
Implementing a OneForOne Strategy
Most of the time, you'll want a OneForOneStrategy. This means the supervisor handles each failing child independently. If one child crashes, it doesn't affect its siblings. I generally prefer this because it limits the "blast radius" of a failure.
import akka.actor.SupervisorStrategy._
import akka.actor.{Actor, OneForOneStrategy, Props}
import scala.concurrent.duration._
class ManagerActor extends Actor {
override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
case _: ArithmeticException => Resume
case _: NullPointerException => Restart
case _: Exception => Escalate
}
def receive = {
case "create-worker" => context.actorOf(Props[WorkerActor], "worker")
}
}
class WorkerActor extends Actor {
def receive = {
case "fail" => throw new NullPointerException("Something went wrong!")
}
}
Notice how the ManagerActor doesn't care how the WorkerActor does its job; it only cares about what happens when it fails. By separating the business logic (in the worker) from the fault-tolerance logic (in the manager), your system becomes significantly more resilient. You can change your recovery policy in one place without touching the complex logic inside your workers.
There is also an AllForOneStrategy, where if one child fails, the supervisor restarts all children. I'll warn you: use this sparingly. It's only appropriate when your children are so tightly coupled that one cannot function if another has failed. In 95% of the systems I've built, OneForOneStrategy is the right tool for the job.
📋 Practical Task
Exercise: Building a Resilient Weather Data Aggregator
You are tasked with building a system that fetches weather data from multiple unreliable external APIs. You need to implement a supervision hierarchy that prevents a single failing API call from crashing your entire aggregator.
Requirements:
- Create a
WeatherSupervisoractor. - Create a
WeatherFetcheractor that simulates an API call. When it receives the message"fetch", it should randomly throw either aTimeoutExceptionor aMalformedURLException. - Implement a
OneForOneStrategyin theWeatherSupervisor:- If a
TimeoutExceptionoccurs, the actor should Restart (assuming a fresh connection might work). - If a
MalformedURLExceptionoccurs, the actor should Stop (since a bad URL won't fix itself by restarting). - Any other exception should be Escalated.
- If a
- Start the system by having the
WeatherSupervisorcreate threeWeatherFetcherchildren and sending them"fetch"messages in a loop.
Goal: Verify through logs that TimeoutExceptions trigger a restart (you'll see the actor's preStart or constructor run again), while MalformedURLExceptions kill the specific actor without affecting the other fetchers.
There are no comments for now.