Ox gives Scala a direct-style structured concurrency without the Future maze
Safe direct-style streaming, concurrency and resiliency for Scala on the JVM
At a glance
- What is it?
- 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.
- Who is it for?
- 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.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Scala, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
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.
"com.softwaremill.ox" %% "core" % "1.1.0"With scala-cli it goes in as a using directive.
//> 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.
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.
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 & doneAnd race returns the first successful result, with the same guarantee about the losing branch.
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 resultThe 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.
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.
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.
def computationR: Int = ???
retry(Schedule.exponentialBackoff(100.millis).maxRetries(4)
.jitter().maxInterval(5.minutes))(computationR)Repeat uses a fixed interval instead.
def computationR: Int = ???
repeat(Schedule.fixedInterval(100.millis))(computationR)A rate limiter is created inside a supervised scope and used with runBlocking.
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.
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.
Editorial 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.
Frequently asked questions
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.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/softwaremill-ox)Was this useful?