diff --git a/exercises/solutions-4/.gitignore b/exercises/solutions-4/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..cc92d252fed98e77240201d6120a9b74cb66665a
--- /dev/null
+++ b/exercises/solutions-4/.gitignore
@@ -0,0 +1,8 @@
+.vscode
+.metals
+.bloop
+.bsp
+target
+metals.sbt
+build.properties
+project/project
diff --git a/exercises/solutions-4/.scalafmt.conf b/exercises/solutions-4/.scalafmt.conf
new file mode 100644
index 0000000000000000000000000000000000000000..1cea5243def8047a81295a8b13cb970660e0b3fc
--- /dev/null
+++ b/exercises/solutions-4/.scalafmt.conf
@@ -0,0 +1,4 @@
+version = "3.4.0"
+runner.dialect = scala3
+rewrite.scala3.convertToNewSyntax = true
+rewrite.scala3.removeOptionalBraces = true
diff --git a/exercises/solutions-4/Readme.md b/exercises/solutions-4/Readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..28b78174dc24ddad1a642eac71bd873b61501ab6
--- /dev/null
+++ b/exercises/solutions-4/Readme.md
@@ -0,0 +1,4 @@
+# Exercise Session 4, Solutions
+
+- Problem 1: [Problem1.scala](src/main/scala/Problem1.scala) and [Problem1Test.scala](src/test/scala/Problem1Test.scala)
+- Problem 2: [Problem2.scala](src/main/scala/Problem2.scala)
diff --git a/exercises/solutions-4/build.sbt b/exercises/solutions-4/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..9865ad5b4683b009fb6b2a820e5368c86adfb891
--- /dev/null
+++ b/exercises/solutions-4/build.sbt
@@ -0,0 +1,22 @@
+val scala3Version = "3.1.2"
+val akkaVersion = "2.6.19"
+
+lazy val root = project
+  .in(file("."))
+  .settings(
+    name := "code",
+    version := "0.1.0-SNAPSHOT",
+    scalaVersion := scala3Version,
+    fork := true,
+    javaOptions ++= Seq(
+      "-Dakka.loglevel=Debug",
+      "-Dakka.actor.debug.receive=on"
+    ),
+    libraryDependencies ++= Seq(
+      "com.typesafe.akka" %% "akka-actor" % akkaVersion,
+      "com.typesafe.akka" %% "akka-testkit" % akkaVersion,
+      "junit" % "junit" % "4.13" % Test,
+      "com.github.sbt" % "junit-interface" % "0.13.3" % Test
+    ),
+    Test / testOptions += Tests.Argument(TestFrameworks.JUnit)
+  )
diff --git a/exercises/solutions-4/project/plugins.sbt b/exercises/solutions-4/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..83adafa3f1dc8a82b2f18ba573029a8f1ca4cc72
--- /dev/null
+++ b/exercises/solutions-4/project/plugins.sbt
@@ -0,0 +1 @@
+addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
diff --git a/exercises/solutions-4/src/main/scala/Problem1.scala b/exercises/solutions-4/src/main/scala/Problem1.scala
new file mode 100644
index 0000000000000000000000000000000000000000..a86a8f358e5702142d139cb59d346f81bd9a36fd
--- /dev/null
+++ b/exercises/solutions-4/src/main/scala/Problem1.scala
@@ -0,0 +1,28 @@
+import scala.util.{Try, Success, Failure}
+import scala.concurrent.ExecutionContext
+import java.util.concurrent.Future
+import java.util.concurrent.atomic.AtomicReference
+
+// Test using `sbt "testOnly Problem1Test"`
+// See tests in Problem1Test.scala
+
+trait MyFuture[+T]:
+  def onComplete(callback: Try[T] => Unit): Unit
+
+extension [T](self: MyFuture[T])
+  def map[S](f: T => S): MyFuture[S] =
+    new MyFuture:
+      def onComplete(callback: Try[S] => Unit): Unit =
+        self.onComplete {
+          case Success(v) => callback(Success(f(v)))
+          case Failure(e) => callback(Failure(e))
+        }
+  def filter(p: T => Boolean): MyFuture[T] =
+    new MyFuture:
+      def onComplete(callback: Try[T] => Unit): Unit =
+        self.onComplete {
+          case Success(v) =>
+            if p(v) then callback(Success(v))
+            else callback(Failure(new NoSuchElementException()))
+          case Failure(e) => callback(Failure(e))
+        }
diff --git a/exercises/solutions-4/src/main/scala/Problem2.scala b/exercises/solutions-4/src/main/scala/Problem2.scala
new file mode 100644
index 0000000000000000000000000000000000000000..8ab4f787875459c64de56dbf2fbe5ab5392476e7
--- /dev/null
+++ b/exercises/solutions-4/src/main/scala/Problem2.scala
@@ -0,0 +1,72 @@
+import akka.actor.{Actor, Props, ActorSystem, ActorRef, ActorLogging}
+import akka.testkit.{TestKit, ImplicitSender}
+import akka.event.LoggingReceive
+
+// Run using `sbt "runMain problem2"`
+
+/** Type of messages exchanged between our Actors.
+  *
+  * Note: enumerations are the Scala 3 idiomatic syntax to define algebraic data
+  * types (ADTs). The code below is desugared to something equivalent to:
+  *
+  * ```
+  * trait Message
+  * case class Request(computation: () => Unit) extends Message
+  * object Ready extends Message
+  * ```
+  *
+  * which is the syntax used in the lecture videos.
+  *
+  * Read also:
+  *   - Translation of Enums and ADTs:
+  *     https://docs.scala-lang.org/scala3/reference/enums/desugarEnums.html
+  *   - Enums slides from CS210:
+  *     https://gitlab.epfl.ch/lamp/cs210/-/blob/master/slides/progfun1-4-4.pdf
+  */
+enum Message:
+  case Request(computation: () => Unit)
+  case Ready
+import Message.*
+
+class Coordinator extends Actor:
+  var availableWorkers: List[ActorRef] = Nil
+  var pendingRequests: List[Request] = Nil
+
+  override def receive = LoggingReceive {
+    case Ready =>
+      if pendingRequests.isEmpty then
+        availableWorkers = availableWorkers :+ sender()
+      else
+        val request = pendingRequests.head
+        pendingRequests = pendingRequests.tail
+        sender() ! request
+    case request: Request =>
+      availableWorkers match
+        case worker :: rest =>
+          worker ! request
+          availableWorkers = rest
+        case Nil =>
+          pendingRequests = pendingRequests :+ request
+  }
+
+class Worker(coordinator: ActorRef) extends Actor:
+  coordinator ! Ready
+
+  override def receive: Receive = LoggingReceive { case Request(f) =>
+    f()
+    coordinator ! Ready
+  }
+
+@main def problem2 = new TestKit(ActorSystem("coordinator-workers"))
+  with ImplicitSender:
+  try
+    val coordinator = system.actorOf(Props(Coordinator()), "coordinator")
+    val workers = Seq.tabulate(4)(i =>
+      system.actorOf(Props(Worker(coordinator)), f"worker$i")
+    )
+
+    // Now, clients should be able to send requests to the coordinator…
+    coordinator ! Request(() => println(3 + 5))
+    coordinator ! Request(() => println(67 * 3))
+    // And so on...
+  finally shutdown(system)
diff --git a/exercises/solutions-4/src/test/scala/Problem1Test.scala b/exercises/solutions-4/src/test/scala/Problem1Test.scala
new file mode 100644
index 0000000000000000000000000000000000000000..920c2c8d438618ce9e17a3ac980bf32af2ceeed5
--- /dev/null
+++ b/exercises/solutions-4/src/test/scala/Problem1Test.scala
@@ -0,0 +1,52 @@
+import scala.util.{Try, Success, Failure}
+import org.junit.Test
+import org.junit.Assert.{assertEquals, fail}
+
+class Problem1Test:
+  object MyFuture:
+    final def successful[T](value: T): MyFuture[T] =
+      new MyFuture:
+        def onComplete(callback: Try[T] => Unit): Unit =
+          callback(Success(value))
+
+    final def failed[T](error: Error): MyFuture[T] =
+      new MyFuture:
+        def onComplete(callback: Try[T] => Unit): Unit =
+          callback(Failure(error))
+
+  @Test
+  def mapWorksWithSuccess() =
+    MyFuture.successful(3).map(_ + 1).onComplete {
+      case Success(value) => assertEquals(4, value)
+      case _              => fail("Expected result to be a Success")
+    }
+
+  @Test
+  def mapWorksWithFailure() =
+    val error = new Error("Some error")
+    MyFuture.failed[Int](error).map(_ + 1).onComplete {
+      case Failure(actualError) => assertEquals(error, actualError)
+      case _                    => fail("Expected result to be a Failure")
+    }
+
+  @Test
+  def filterWorksWithSuccessNotFilteredOut() =
+    MyFuture.successful(3).filter(_ == 3).onComplete {
+      case Success(value) => assertEquals(3, value)
+      case _              => fail("Expected result to be a Failure")
+    }
+
+  @Test
+  def filterWorksWithSuccessFilteredOut() =
+    MyFuture.successful(3).filter(_ == 4).onComplete {
+      case Failure(actualError: NoSuchElementException) => ()
+      case _ => fail("Expected result to be a NoSuchElementException exception")
+    }
+
+  @Test
+  def filterWorksWithFailure() =
+    val error = new Error("Some error")
+    MyFuture.failed[Int](error).filter(_ == 3).onComplete {
+      case Failure(actualError) => assertEquals(error, actualError)
+      case _                    => fail("Expected result to be a Failure")
+    }