Contents

Autowire: Zero-Cost Dependency Injection

Adam Warski

18 Sep 2024.4 minutes read

Autowire: Zero-Cost Dependency Injection webp image

autowire is our newest addition to MacWire, a zero-cost, compile-time, type-safe dependency injection library. It provides an additional option for creating object graphs, next to the tried-and-tested wire macro (which has been around since 2013!).

Both wire and autowire incur no runtime overhead. The main mechanism of defining dependencies is via constructor or apply method parameters. MacWire doesn't impact the code of your classes in any way. There are no annotations, no conventions to follow, and definitely no containers that need to do run-time reflection at startup!

The high-level summary of both variants is as follows:

  • autowire creates an instance of the given type using the provided dependencies. Any missing dependencies are created using constructors/apply methods.
  • wire creates an instance of the given type, using dependencies from the context within which it is called. Dependencies are looked up in the enclosing trait/class/object and parents (via inheritance).

The latter is well described in other posts and videos, so let's focus on the newly added variant, which supports Scala 3 and direct-style.

To be precise, autowire is not entirely new. A variant specialized for cats-effect and Scala 2, yielding a cats-effect Resource, has been available for some time. You can read all about it in Mateusz's Borek blog post.

Quick example

Let's look at a quick example. Say we have a couple of classes implementing various layers of our application, some containing infrastructure code and some—business logic. Each such class expresses dependencies using constructor parameters.

This ensures that our code is nicely modularised and that responsibilities are appropriately divided. However, at some point, we want to run the application, and we need to create the object graph. Such code is relatively simple but tedious and boring to write.

That's where MacWire comes in. For example:

//> using dep com.softwaremill.macwire::macros:2.6.1

// service graph
class DatabaseAccess()
class SecurityFilter()
class UserFinder(databaseAccess: DatabaseAccess, 
  securityFilter: SecurityFilter)
class UserStatusReader(userFinder: UserFinder)

// application entry point
@main def main(): Unit =
  import com.softwaremill.macwire.*
  val userStatusReader = autowire[UserStatusReader]()

(the above snippet is runnable using scala-cli, if you'd like to play with it!).

The autowire invocation above is a macro, that is, a program that runs at compile-time and generates code with which the macro invocation is substituted. In our case, the macro generates the object graph wiring code and produces a result equivalent to the following:

val userStatusReader =
  val wiredDatabaseAccess   = new DatabaseAccess()
  val wiredSecurityFilter   = new SecurityFilter()
  val wiredUserFinder       = new UserFinder(wiredDatabaseAccess, 
    wiredSecurityFilter)
  val wiredUserStatusReader = new UserStatusReader(wiredUserFinder)
  wiredUserStatusReader

As you can see, we've already avoided writing some boilerplate, and that's a really small object graph—typically, an application has tens, if not hundreds, of services.

How autowire works

autowire takes a single type parameter—the type of the class for which an instance should be created. To create the instance, the public primary constructor is used, or if one is absent—the apply method from the companion object. Any dependencies are created recursively.

However, while dependencies can be created via constructors most of the time, that's not always true. That's why you can provide various "directives", as to how to create instances of certain types. This is done by passing parameters to the autowire invocation. Each parameter might be:

  • a specific instance to use
  • a function to create an instance
  • a class to instantiate to provide a dependency for the types it implements, classOf[SomeType]
  • an autowireMembersOf(instance) call to use the members of the given instance as dependencies

As an example, we might have a DataSource, for which we need to manage the life-cycle, and hence instantiate by hand. There are also some flags that we need to pass when creating the UserFinder; hence, we need to provide a custom function using which instances of this class should be created. Finally, SecurityFilter is a trait, and we want to use the SecurityFilterImpl class that implements it. Thus, our example becomes:

//> using dep com.softwaremill.macwire::macros:2.6.1

import java.io.Closeable

class DataSource(jdbcConn: String) extends Closeable:
  def close() = ()
class DatabaseAccess(ds: DataSource)
trait SecurityFilter
class SecurityFilterImpl() extends SecurityFilter
class UserFinder(databaseAccess: DatabaseAccess, 
  securityFilter: SecurityFilter, adminOnly: Boolean)
class UserStatusReader(userFinder: UserFinder)

@main def main(): Unit =
  import com.softwaremill.macwire.*
  import scala.util.Using

  Using.resource(DataSource("jdbc:h2:~/test")): ds =>
    autowire[UserStatusReader](
      ds,
      classOf[SecurityFilterImpl],
      UserFinder(_, _, adminOnly = true)
   )

Error messages

Things will go wrong quite often. For example, a dependency might be missing. MacWire tries to help you out in such situations and provides meaningful error messages containing the wiring path that leads to a given type.

Let's say one of the constructors in our previous example is private; the error message points to how we got there:

class DatabaseAccess private ()
class SecurityFilter()
class UserFinder(databaseAccess: DatabaseAccess, 
  securityFilter: SecurityFilter)
class UserStatusReader(userFinder: UserFinder)

autowire[UserStatusReader]()

// compile-time error:
// cannot find a provided dependency, constructor or apply method for: 
// DatabaseAccess;
// wiring path: UserStatusReader -> UserFinder -> DatabaseAccess

autowire will also report an error if there's a duplicate, unused, or circular dependency. Moreover, primitive types and Strings can't be dependencies (as this might lead to very subtle bugs; use opaque types instead).

Try it out!

MacWire 2.6, including both autowire for direct-style and cats-effect, as well as wire for all Scala versions (JVM, JS, Native, 2.12, 2.13, 3) is available for you to try. If you'd have any feedback, let us know through GitHub issues or the community forum.

Blog Comments powered by Disqus.