Skip to Content
Course content

32: Futures and Promises

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was mentoring a developer who was building a real-time dashboard for a logistics company. He had a feature that needed to fetch data from three different microservices: the fleet status, the driver schedules, and the weather alerts. Initially, he wrote these as three sequential blocking calls. The result? A frustrating 3-second lag on every page load because the app was just sitting there, waiting for the fleet service to finish before it even started asking the driver service for data. He tried to fix it by manually spawning raw Java threads, but he quickly ran into a nightmare of race conditions and synchronization blocks that made the code impossible to read. That's exactly where Scala's Futures come in.

The Promise of a Future Value

In Scala, a Future[T] is essentially a placeholder for a value that doesn't exist yet. Think of it like a claim check at a coat closet; you don't have your jacket right now, but you have a token that guarantees you'll get it once the attendant finds it. The beauty of this is that your main thread doesn't have to stop and stare at the closet door; it can go off and do other work while the "fetching" happens in the background.

To use a Future, you need an ExecutionContext. I like to think of this as the "engine" that actually runs the task. Without it, the Future is just a description of work with no one to actually perform it. In most small projects, you'll see import scala.concurrent.ExecutionContext.Implicits.global, which uses a shared fork-join pool. In a high-traffic production system, however, I'd strongly advise you to define your own thread pools to avoid starving your main application of resources.

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Success, Failure}

def fetchFleetStatus(): Future[String] = Future {
  // Simulate a slow network call
  Thread.sleep(1000)
  "All trucks operational"
}

val statusFuture = fetchFleetStatus()
// The code continues executing here immediately, without waiting for the 1000ms!

Bridging the Gap with Promises

You'll often hear "Future" and "Promise" used interchangeably, but in Scala, they are distinct. A Future is read-only; you can check if it's done or map its result, but you can't "force" a value into it. A Promise[T], on the other hand, is the writable side. It's the handle that allows you to manually complete a Future.

I usually find Promises useful when I'm wrapping an older, callback-based API. If you have a library that says, "I'll call this function when I'm done," you can create a Promise, return its associated Future to your caller, and then call promise.success(value) inside that callback. It's the bridge between the "push" world of callbacks and the "pull" world of Futures.

import scala.concurrent.Promise

val promise = Promise[Int]()
val future = promise.future // This is what you give to the rest of your app

// Later, in some callback or event handler:
promise.success(42) 
// Now, anyone waiting on 'future' is notified that the value is 42.

Composing Asynchronous Workflows

The real power comes when you need to chain these operations. If you have three Futures and you need the results of all three to calculate a final total, you don't want to nest callbacks (the dreaded "callback hell"). Instead, we use for-comprehensions. This makes asynchronous code look and feel like synchronous, sequential code, even though Scala is actually transforming it into a series of non-blocking callbacks under the hood.

One thing to keep in mind: a for-comprehension executes its stages sequentially. If Future B doesn't depend on the result of Future A, don't put them in a for-comprehension, or you'll lose the benefit of parallelism. Start the Futures first, then combine them.

def getDriver(id: Int): Future[String] = Future { s"Driver-$id" }
def getSchedule(driver: String): Future[String] = Future { s"Schedule for $driver" }

val result: Future[String] = for {
  driver <- getDriver(101)           // Wait for driver
  schedule <- getSchedule(driver)    // Then use driver to get schedule
} yield s"Final Report: $schedule"



📋 Practical Task

Exercise: Concurrent Price Aggregator

You are building a travel booking site. You need to fetch the price of a hotel room from three different vendors ( Expedia, Booking.com, and Hotels.com) simultaneously. Because some vendors are slower than others, you cannot afford to call them sequentially.

Your task:

  • Create three functions that return Future[Double]. Each should simulate a network delay using Thread.sleep (use different durations like 500ms, 1000ms, and 1500ms) and return a random price.
  • Write a function findCheapestPrice() that triggers all three requests concurrently.
  • Use a for-comprehension or Future.sequence to gather the results.
  • The final result should be a Future[Double] containing the minimum value among the three vendors.
  • Ensure you use an ExecutionContext to allow the Futures to run.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.