# Ox gives Scala a direct-style structured concurrency without the Future maze

> Ox is an Apache-2.0 licensed Scala 3 library from SoftwareMill that provides direct-style streaming, structured concurrency, scheduling and resiliency primitives for the JVM, requiring JDK 21 or later. Its selling point is expression-oriented code with minimal syntax overhead, developer-friendly stack traces, and a supervised scope whose forks are interrupted when one fails.

**softwaremill/ox** — Safe direct-style streaming, concurrency and resiliency for Scala on the JVM

- Repository: https://github.com/softwaremill/ox
- Website: https://ox.softwaremill.com
- Stars: 527 · Forks: 37
- Language: Scala
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/softwaremill-ox

## What Ox covers, and what it refuses to abstract away

Ox is described as safe direct-style streaming, concurrency and resiliency for Scala on the JVM, requiring JDK 21 and Scala 3. The README lists five areas it covers. Streaming is push-based backpressured streaming designed for direct-style, with a rich set of stream transformations, flexible stream source and sink definitions, and reactive streams integration. Error management covers retries, timeouts, a safe approach to error propagation, and safe resource management. Concurrency covers high-level concurrency operators, developer-friendly structured concurrency, safe low-level primitives, and communication between concurrently running computations. Then scheduling and timers, and resiliency with circuit breakers, bulkheads, rate limiters and backpressure. The philosophy is stated plainly: Ox enables writing simple, expression-oriented code in functional style, with the syntax overhead kept to a minimum, preserving developer-friendly stack traces, and without compromising performance. That last phrase is the whole pitch. Most Scala concurrency libraries are either safe and verbose or fast and unsafe, and the argument here is that neither trade-off is necessary.

## Adding the dependency

There is one artifact, core, and it is on Maven Central. With sbt the declaration is a single line.

```scala
"com.softwaremill.ox" %% "core" % "1.1.0"
```

With scala-cli it goes in as a using directive.

```scala
//> using dep "com.softwaremill.ox::core:1.1.0"
```

The version tracks the release, and the release list shows v1.1.0 on 2026-10-19, v1.0.9 on 2026-10-01 and v1.0.8 on 2026-09-23, so the library is moving quickly through patch numbers. The documentation lives at ox.softwaremill.com and the ScalaDocs are on javadoc.io. The repository layout shows that this is a multi-module project rather than one library. There is a core directory, and then separate modules for cron, flow-json, flow-reactive-streams, kafka, mdc-logback and otel-context, which tells you the streaming and concurrency pieces are separable from the integrations around them.

## Parallel, timeout and race, in three expressions

The tour in the README shows the three operations that replace most of what people reach for Future for. Running two computations in parallel is a single call.

```scala
def computation1: Int = { sleep(2.seconds); 1 }
def computation2: String = { sleep(1.second); "2" }
val result1: (Int, String) = par(computation1, computation2)
// (1, "2")
```

Timeout wraps a computation and returns an Either, with the caveat the comment calls out.

```scala
def computation3: Int = { sleep(2.seconds); 1 }
val result2: Either[TimeoutException, Int] = timeout(1.second)(computation3).catching[TimeoutException]
// `timeout` only completes once the loosing branch is interrupted & done
```

And race returns the first successful result, with the same guarantee about the losing branch.

```scala
def computation4: Int = { sleep(2.seconds); 1 }
def computation5: Int = { sleep(1.second); 2 }
val result3: Int = raceSuccess(computation4, computation5)
// as before, the loosing branch is interrupted & awaited before returning a result
```

The repeated comment about the losing branch being interrupted and awaited is the important detail. It means the operators do not leave a computation running in the background after returning, which is the usual leak in timeout and race implementations.

## The supervised scope, and let it crash

Structured concurrency is the part that justifies the library, and the README calls it developer-friendly. The supervised scope with fork and join is equivalent to par.

```scala
supervised {
  val f1 = fork { sleep(2.seconds); 1 }
  val f2 = fork { sleep(1.second); 2 }
  (f1.join(), f2.join())
}
```

The failure behaviour is explicit in the comments of the second example.

```scala
supervised {
  forkUser:
    sleep(1.second)
    println("Hello!")

  forkUser:
    sleep(500.millis)
    throw new RuntimeException("boom!")
}
// on exception, all other forks are interrupted ("let it crash")
// the scope ends & re-throws only when all forks complete (no "leftovers")
```

Two properties fall out of that. The first is that a failure in one fork interrupts the others, so a partially completed batch does not leave half the work running. The second is that the scope waits for every fork to finish before it re-throws, so there are no leftovers when the error propagates. The label forkUser marks a fork as a user-facing fork, which is how the supervision hierarchy distinguishes a failure that should take down the scope from one that should be handled.

## Retries, repeats, rate limits and resources

Scheduling and resiliency are built on the same scope machinery. Retry takes a schedule with exponential backoff, a retry count, jitter and a maximum interval.

```scala
def computationR: Int = ???
retry(Schedule.exponentialBackoff(100.millis).maxRetries(4)
  .jitter().maxInterval(5.minutes))(computationR)
```

Repeat uses a fixed interval instead.

```scala
def computationR: Int = ???
repeat(Schedule.fixedInterval(100.millis))(computationR)
```

A rate limiter is created inside a supervised scope and used with runBlocking.

```scala
supervised:
  val rateLimiter = RateLimiter.fixedWindowWithStartTime(2, 1.second)
  rateLimiter.runBlocking({ /* ... */ })
```

And resource management is handled by the scope itself, where a closeable is acquired inside a scope and closed when the scope ends whether it succeeded or failed.

```scala
supervised {
  val writer = useCloseableInScope(new java.io.PrintWriter("test.txt"))
  // ... use writer ...
} // writer is closed when the scope ends (successfully or with an error)
```

That last one removes a class of try-finally noise that appears in every Scala program that opens a resource and forks work at the same time.

## Flows, actors and channels

The streaming half is a Flow type with a functional transformation API, and the examples show both the pure and the effectful sides. A simple flow chains iterate, filter, map, intersperse, a stateful map and take, then runs with runForeach. Flows that do I/O can map in parallel with mapPar and drain with runDrain, and two flows can be merged with merge, which the README says properly handles the failure of either branch. There is also an imperative integration point through usingEmit, where a callback drives emission from an existing loop. For inter-fork communication there are buffered and rendezvous channels, with send, done and a select over send and receive clauses in the Go style. Actors are type-safe and scoped, so a reference can be shared across forks but only within the concurrency scope. And OxApp gives an application object that shuts down cleanly when interrupted with SIGINT or SIGTERM, with a fork available for top-level background threads.

## Where Ox is the wrong choice

Three limits. The first is the floor. Scala 3 and JDK 21 or later are both required, so a team on Scala 2 or on JDK 17 is looking at a two-step migration before it can even try the library, and the Scala 3 syntax in the examples, with significant indentation and the given and using clauses, is not a small adjustment either. The second is that Ox is a Scala library rather than a protocol or a specification, so it competes with other Scala libraries rather than with a standard. If your team has already standardised on ZIO, cats-effect or Akka Streams, adopting Ox means a second concurrency idiom in the same codebase, and the boundary between the two becomes a source of bugs rather than a simplification. The third is that the direct-style promise cuts both ways. Direct style means you write ordinary-looking code, which means when something goes wrong at the concurrency level the stack trace is shorter and less obvious than a monadic one would be, and the operators quietly enforce ordering and interruption semantics that are not visible at the call site. That is a genuine trade-off rather than a defect, but it is the trade you are making when you choose it.

## Conclusion

Adopt Ox if you are on Scala 3 and JDK 21 and find Future and Effect syntax noisy, since the same operations read as ordinary expressions and the supervised scope removes a class of concurrency bug. Skip it if you are on Scala 2 or an older JDK, since the library requires both Scala 3 and JDK 21 or later. Before you adopt it, read the flow and concurrency sections separately, because the streaming half and the concurrency half have different learning curves.

## FAQ

### What is Ox and what does it require?

Ox is a safe direct-style streaming, concurrency and resiliency library for Scala on the JVM, covering streaming, error management, concurrency, scheduling and timers, and resiliency with circuit breakers, bulkheads, rate limiters and backpressure. It requires JDK 21 or later and Scala 3.

### How do I add Ox to an sbt project?

With the dependency `"com.softwaremill.ox" %% "core" % "1.1.0"` in sbt, or with the directive `//> using dep "com.softwaremill.ox::core:1.1.0"` in scala-cli. The artifact is published on Maven Central as com.softwaremill.ox core.

### What happens when a fork fails inside an Ox supervised scope?

All other forks are interrupted, following the let it crash approach, and the scope ends and re-throws only when all forks have completed, so no leftovers are left behind. The comment in the README also notes that the scope waits for every fork to finish before the error propagates.

### Does Ox provide streaming as well as concurrency?

Yes. Streaming is described as push-based backpressured streaming designed for direct-style, with a rich set of stream transformations, flexible stream source and sink definitions and reactive streams integration, and the repository contains a flow-reactive-streams module alongside the core one.

## Sources

- [License: Apache-2.0](https://github.com/softwaremill/ox/blob/master/LICENSE)
- [Project website](https://ox.softwaremill.com)
- [README](https://github.com/softwaremill/ox/blob/master/README.md)
- [Releases](https://github.com/softwaremill/ox/releases)
- [softwaremill/ox on GitHub](https://github.com/softwaremill/ox)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/softwaremill-ox
