scalaintermediate

Dependency Injection Patterns

Implement dependency injection in Scala: cake pattern, constructor injection, and ZIO ZLayer.

scala
// Pattern 1: Constructor injection (simplest)
trait UserRepository:
  def findById(id: Long): Option[String]
  def save(name: String): Long

trait EmailService:
  def send(to: String, subject: String, body: String): Unit

class InMemoryUserRepo extends UserRepository:
  private var users = Map(1L -> "Alice", 2L -> "Bob")
  private var nextId = 3L

  def findById(id: Long): Option[String] = users.get(id)
  def save(name: String): Long =
    val id = nextId; nextId += 1
    users += (id -> name); id

class ConsoleEmailService extends EmailService:
  def send(to: String, subject: String, body: String): Unit =
    println(s"  Email to $to: $subject")

// Service with constructor-injected dependencies
class UserService(repo: UserRepository, email: EmailService):
  def register(name: String): Long =
    val id = repo.save(name)
    email.send(name, "Welcome", s"Hello $name, your ID is $id")
    id

  def getUser(id: Long): Option[String] = repo.findById(id)

// Pattern 2: Cake pattern (self-types)
trait UserRepositoryComponent:
  val userRepository: UserRepository

trait EmailServiceComponent:
  val emailService: EmailService

trait UserServiceComponent:
  self: UserRepositoryComponent with EmailServiceComponent =>

  lazy val userService = new UserService(userRepository, emailService)

// Wiring
object ProductionEnvironment
    extends UserServiceComponent
    with UserRepositoryComponent
    with EmailServiceComponent:
  val userRepository: UserRepository = InMemoryUserRepo()
  val emailService: EmailService = ConsoleEmailService()

// Pattern 3: Simple reader/context
trait AppContext:
  def userRepo: UserRepository
  def emailSvc: EmailService

def registerUser(name: String)(using ctx: AppContext): Long =
  val id = ctx.userRepo.save(name)
  ctx.emailSvc.send(name, "Welcome", s"Your ID: $id")
  id

def getUser(id: Long)(using ctx: AppContext): Option[String] =
  ctx.userRepo.findById(id)

@main def run(): Unit =
  // Constructor injection
  println("=== Constructor Injection ===")
  val repo = InMemoryUserRepo()
  val email = ConsoleEmailService()
  val service = UserService(repo, email)
  val id = service.register("Carol")
  println(s"Registered: ${service.getUser(id)}")

  // Cake pattern
  println("\n=== Cake Pattern ===")
  val id2 = ProductionEnvironment.userService.register("Dave")
  println(s"Registered: ${ProductionEnvironment.userService.getUser(id2)}")

  // Context/Reader
  println("\n=== Context Pattern ===")
  given AppContext with
    def userRepo = InMemoryUserRepo()
    def emailSvc = ConsoleEmailService()

  val id3 = registerUser("Eve")
  println(s"Registered: ${getUser(id3)}")

Use Cases

  • Decoupled service architecture
  • Testable component design
  • Modular application wiring

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.