💬 Questions / comments / suggestions are welcome in the discussions, or feel free to contact me directly.
Demonstrates using Spring Framework with Scala 3.
This codebase attempts to provide a sample project that includes working implementations of the most commonly used elements in a performant modern REST API (authn/authz, JWT, async programming, unit & integration tests, etc.) while remaining as simple and easy to understand as possible.
There probably isn't a universally great answer. Anyone checking out this codebase probably already has their own valid reasons for wanting to use Scala. I will say that while the learning curve may be high, assuming a team of strong developers, Scala is certainly one of if not the most productive JVM languages to exist.
Everyone else interested in using Spring with something other than Java, consider using Kotlin instead.
It's no secret that the design of Kotlin draws quite a bit of inspiration from Scala, and more recently, Scala3 has begun to take inspiration from Kotlin as well,
so there is some convergence happening. Throw in a functional programming library like Arrow and you have a compelling alternative
that plays a little better with Spring's Java codebase, has a lower learning curve and a larger pool of active developers.
The biggest gripe I have with Kotlin when it comes to backend systems has to do with database libraries.
There are many to choose from, but due to laguage limitations (last I checked) do not offer the same combination of
expressiveness, functionality, and type safety of something like Doobie. How big of a deal that really is depends on the project and the team.
The biggest headaches of moving from Scala 2.x to Scala 3 have been with the level of compatibility tools like IntellIJ offer, and the relatively small market share it has of Scala projects and libraries. Builds sometimes slow to a crawl, hang, or randomly fail, only to succeed after a second or third retry.
The jury is still out on whether the migration is worthwhile for established projects, but I do feel that Scala3 has reached the point where it is the better choice for new projects.
For convenience this project includes a docker-compose environment that provisions a postgres database and configures the app to use it.
There is also a K6 benchmark jetpack-compose environment in /extras
that is preconfigured to run against localhost:8080.
See the README for more information.
- Gradle1
- Spring Boot
- Spring Security
- JWT Scala
- Circe - JSON serialization and deserialization
ZIO(Sticking with with Cats Effect out of preference, and because ZIO seems to have been abandoned by its author.)- Cats Effect
- Doobie2
- Flyway - Database schema definitions and migrations
- ScalaTest
- Mockito
As far as getting Spring and Scala to play nicely together, the challenges are fairly minor and can either be resolved once in your codebase and forgotten about, or are things that just take time getting used to.
The big one one to be aware of here relates to async programming. If you plan to use any form of Scala flavored structured concurrency, whether it be Futures, Cats Effect, ZIO, or something else, you need to adapt the Spring async programming model to work with it or performance will suffer.
Since we're using Scala and Spring is written in Java, we have to handle some idiosyncrasies. These are a few of the most common ones I have run into.
Since Spring and JUnit rely heavily on annotations, this is going to be one that you will run into a lot.
A REST endpoint in Java:
@GetMapping(path = "/test")
public JsonNode test() {...}
Looks like this in Scala:
@GetMapping(path = Array("/test"))
def test(): String = {...}
Java and Scala collections are generally not interchangeable. In most cases handling this is as simple as importing the necessary converter and using it. More info available here.
As of this writing and so far as I am aware, Spring still uses null instead of Java 8's Optional monad. This means that whenever you are working with variables coming Spring, you generally want to wrap them in a Scala Option.
Watch out for this when using Spring's @RequestParam
and @PathVariable
annotations in controllers.
Parts of Spring's architecture relies on ThreadLocal context, particularly when using WebMVC. This can be problematic when interfacing between things like controller entry points and services and utilities that are built around IO/Future/ZIO etc. monads. Effectively, trying to access something like Spring Security's SecurityContext from these methods will not work. Without going into too much detail WebFlux has the same basic problem, even though its not technically using ThreadLocal context.
My preferred solution is to pass the SecurityContext and any other ThreadLocal / pseudo global context data as an argument to these methods.
Spring has it's own mechanisms for async programming, and it takes some work to adapt it to be compatible with IO monads. Even after adapting these mechanisms we are left with having to manage an additional threadpool(s) to accommodate Spring. Another challenge here is adapting the handling of uncaught exceptions so that Spring's conventional mechanisms will continue to function.
Anecdotally, I soured on RxJava quite some time ago due what I consider to be poor design. The complete laundry list is out of scope but by way of a couple examples, no common base for reactive types, and the inability to emit null values.
While they share similar syntax, Reactor is not RxJava, and even fixes some of its mistakes. From a developer productivity perspective I find many other tools to be more expressive, less "magical" and more extensible; cats-effect, ZIO, and Kotlin coroutines with Flow (if you use Kotlin) to name a few.
The original version of this project used WebMVC which is built on top of Apache Tomcat and has pseudo-async support.
I've since switched to using WebFlux which uses Netty and is generally more performant, particularly when under heavy load.
I would not be surprised if this changes in the future thanks to the work being done on Project Loom.
For those interested in exploring this further, check out the old webmvc tagof this repository.
This project uses Doobie, which is built on top of JDBC which is synchronous but supports a wide variety SQL databases.
There is another library, Skunk, which is written by the same author and offers similar functionality.
It's fully asynchronous but locks you into Postgres.
Another option would be to use R2DBC which is also async. I've not tried this approach yet but imagine it could be wrapped with cats-effect IO similarly to what was done with [Mono] in the controller layer.
Right now this conversion is done in the controller layer like this:
private given ioToMono[A](): Conversion[IO[A], Mono[A]] with {
def apply(io: IO[A]): Mono[A] = {
Mono.fromFuture(new CompletableFuture[A]().tap { cf =>
io.unsafeRunAsync {
case Right(value) => cf.complete(value)
case Left(error) => cf.completeExceptionally(error)
}
})
}
}
There may be performance issues going between the WebFlux and Cats Effect threadpools, particularly when running in an environment with a small number of CPU cores. This has to do with how time sharing is handled with fibers / virtual threads; in Cats Effect, a fiber generally only yields on an IO boundary, or in other words, when one of it's composite IO blocks completes. This is fundamentally different from true threads where the OS scheduler ensures each thread gets a fair amount of CPU time regardless of where / what each thread is doing.
Ideally there is at least one CPU core dedicated to each thread in each pool, but I am not sure how thread affinity works here or if it is even possible to exclusively bind a CPU to each of these threads. I also plan to provide better detail on what happens under load in situations where there are fewer cores than threads, or even a single core.
- Add OAuth2 request/ refresh tokens
- Add Oauth2 client to support third party authentication (Google, etc)
- Add JTI to JWT tokens to support revocation
Footnotes
-
I've used for several Scala projects now and have few regrets. SBT I believe might have some minor performance benefits, but you really cant beat Gradle in terms of features and support. Having said that, it is possible that some of the build instability / Intellij bugginess I am experiencing is due to Gradle. ↩
-
So why Doobie and not one of the options that come packaged with Spring? Two main reasons: 1) Integrates seemlessly with Cats Effect and the IO monad, which is my preferred tool for structured concurrency. 2) Doobie is oriented around writing pure SQL and producing results as immutable case classes which I prefer over ORM approaches etc. that involve things like Hibernate, JPA, "live objects", etc. ↩