A “Role” is an entrypoint for a specific application hosted in a larger software suite. Bundling multiple roles in a single .jar file can simplify deployment and operations.

distage-framework module contains the distage Role API:

libraryDependencies += "io.7mind.izumi" %% "distage-framework" % "0.10.19"

With default RoleAppLauncherImpl, roles to launch are specified on the command-line: ./launcher role1 role2 role3. Only the components required by the specified roles will be created, everything else will be pruned. (see: GC)

Two roles are bundled by default: Help and ConfigWriter.

Further reading: Roles: a viable alternative to Microservices

Typesafe Config

distage-extension-config library allows summoning case classes and sealed traits from typesafe-config configuration

To use it, add distage-extension-config library:

libraryDependencies += "io.7mind.izumi" %% "distage-extension-config" % "0.10.19"

Use helper functions in ConfigModuleDef to parse the Typesafe Config instance bound to AppConfig into case classes:

import distage.{DIKey, Roots, ModuleDef, Id, Injector}
import distage.config.{AppConfig, ConfigModuleDef}
import com.typesafe.config.ConfigFactory

final case class Conf(name: String, age: Int)

final case class OtherConf(other: Boolean)

final class ConfigPrinter(conf: Conf, otherConf: OtherConf @Id("other")) {
  def print() = {
    println(s"name: ${conf.name}, age: ${conf.age}, other: ${otherConf.other}")

val module = new ConfigModuleDef {

  // declare paths to parse
  // add config instance
    """conf {
      |  name = "John"
      |  age = 33
      |  other = true
// module: AnyRef with ConfigModuleDef = 
// make[{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.ConfigPrinter}].from(call(Class(repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.Conf, repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.OtherConf): repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.ConfigPrinter)) ((distage-framework.md:34))
// make[{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.Conf}].from(call(izumi.distage.config.ConfigModuleDef$$$Lambda$20586/1395765466@3b738a5f(izumi.distage.config.model.AppConfig): repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.Conf)).tagged(Set(ConfTag(conf))) ((distage-framework.md:37))
// make[{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.OtherConf@other}].from(call(izumi.distage.config.ConfigModuleDef$$$Lambda$20586/1395765466@3e828a71(izumi.distage.config.model.AppConfig): repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.OtherConf)).tagged(Set(ConfTag(conf))) ((distage-framework.md:38))
// make[{type.izumi.distage.config.model.AppConfig}].from(call(izumi.distage.model.definition.dsl.ModuleDefDSL$MakeDSLBase$$Lambda$20054/1288318063@7a8c1116(): izumi.distage.config.model.AppConfig)) ((distage-framework.md:41))

Injector().produceRun(module) {
  configPrinter: ConfigPrinter =>
// name: John, age: 33, other: true

Automatic derivation of config codecs is based on pureconfig-magnolia. Pureconfig codecs for a type will be used if they exist.

You don’t have to explicitly make[AppConfig] in distage-testkit’s tests and in distage-framework’s Roles, unless you want to override default behavior. By default, tests and roles will try to read the configurations from resources with the following names, in order:

- $roleName.conf
- $roleName-reference.conf
- $roleName-reference-dev.conf
- application.conf
- application-reference.conf
- application-reference-dev.conf
- common.conf
- common-reference.conf
- common-reference-dev.conf

Where distage-testkit uses TestConfig#testBaseName instead of roleName.

When explicit configs are passed to the role launcher on the command-line using the -c option, they have higher priority than all the reference configs. Role-specific configs on the command-line (-c option after :role argument) override global command-line configs (-c option given before the first :role argument).


  ./launcher -c global.conf :role1 -c role1.conf :role2 -c role2.conf

Here configs will be loaded in the following order, with higher priority earlier:

  • explicits: role1.conf, role2.conf, global.conf,
  • resources: role1[-reference,-dev].conf, role2[-reference,-dev].conf, ,application[-reference,-dev].conf, common[-reference,-dev].conf


distage-extension-plugins module adds classpath discovery for modules that inherit a marker trait PluginBase. Plugins enable extreme late-binding; e.g. they allow a program to extend itself at launch time with new Plugin classes on the classpath. Plugins are compatible with compile-time checks as long as they’re defined in a separate module.

To use plugins, first add the distage-extension-plugins library:

libraryDependencies += "io.7mind.izumi" %% "distage-extension-plugins" % "0.10.19"

Create a module extending the PluginDef trait instead of ModuleDef:

// package com.example.petstore

import distage._
import distage.plugins._

object PetStorePlugin extends PluginDef {

Collect all PluginDefs in a package:

val pluginConfig = PluginConfig.cached(
  packagesEnabled = Seq("com.example.petstore") // packages to scan
// pluginConfig: PluginConfig = PluginConfig(List(com.example.petstore),List(),true,false,List(),List())

val appModules = PluginLoader().load(pluginConfig)
// appModules: Seq[PluginBase] = List(
// make[{type.com.example.petstore.PetRepository}].from(call(Class(): com.example.petstore.PetRepository)) ((PetStorePlugin.scala:6))
// make[{type.com.example.petstore.PetStoreService}].from(call(Class(): com.example.petstore.PetStoreService)) ((PetStorePlugin.scala:7))
// make[{type.com.example.petstore.PetStoreController}].from(call(Class(): com.example.petstore.PetStoreController)) ((PetStorePlugin.scala:8)))

Execute collected modules as usual:

// combine all modules into one

val appModule = appModules.merge
// appModule: PluginBase = 
// make[{type.com.example.petstore.PetRepository}].from(call(Class(): com.example.petstore.PetRepository)) ((PetStorePlugin.scala:6))
// make[{type.com.example.petstore.PetStoreService}].from(call(Class(): com.example.petstore.PetStoreService)) ((PetStorePlugin.scala:7))
// make[{type.com.example.petstore.PetStoreController}].from(call(Class(): com.example.petstore.PetStoreController)) ((PetStorePlugin.scala:8))

// launch

// PetStoreController: running!
// res4: izumi.fundamentals.platform.functional.package.Identity[Unit] = ()

Compile-time checks

An experimental compile-time verification API is available in the distage-framework module.

To use it add distage-framework library:

libraryDependencies += "io.7mind.izumi" %% "distage-framework" % "0.10.19"

Only plugins defined in a different module can be checked at compile-time, test scope counts as a different module.


In main scope:

// package com.example

import distage.DIKey
import distage.StandardAxis.Env
import distage.config.ConfigModuleDef
import distage.plugins.PluginDef
import izumi.distage.staticinjector.plugins.ModuleRequirements

final case class HostPort(host: String, port: Int)

final case class Config(hostPort: HostPort)

final class Service(conf: Config, otherService: OtherService)
final class OtherService

// error: OtherService is not bound here, even though Service depends on it
final class AppPlugin extends PluginDef with ConfigModuleDef {

// Declare OtherService as an external dependency
final class AppRequirements extends ModuleRequirements(
  // If we remove this line, compilation will rightfully break

In config:

// src/main/resources/application.conf
config {
  host = localhost
  port = 8080

In test scope:

// package com.example.test

import com.example._
import org.scalatest.wordspec.AnyWordSpec
import izumi.distage.staticinjector.plugins.StaticPluginChecker

final class AppPluginTest extends AnyWordSpec {
  "App plugin will work (if OtherService is provided later)" in {
    StaticPluginChecker.checkWithConfig[AppPlugin, AppRequirements]("env:prod", ".*.application.conf")   

checkWithConfig will run at compile-time whenever AppPluginTest is recompiled.

Note: Since version 0.10.0, configuration files are no longer checked for correctness by the compile-time checker, see: https://github.com/7mind/izumi/issues/763