
zio-dependency-injection
by connectstrata
Skill files for coding agents working with Scala
SKILL.md
name: zio-dependency-injection description: Standardized patterns for ZIO dependency injection using the ZIO service pattern with ZLayer.derive. Use when creating new ZIO service classes, refactoring existing ZIO code to follow this pattern, reviewing code that incorrectly uses ZIO.service in business logic, or when working with ZIO layers and dependency injection. Apply this pattern for all service classes in the Scala backend.
ZIO Dependency Injection
Overview
Use the ZIO service pattern where all class dependencies are injected as constructor parameters and every service class has a layer built using ZLayer.derive, which automatically wires up constructor parameters.
Key principle: Only use ZIO.service accessor methods in main application files for composing the application, or for accessing contextual information from the environment (like MemberAuthContext). Never use them in service implementations.
The Service Pattern
Structure
Every service follows this three-part structure:
- Trait: Defines the service interface
- Case class implementation: All dependencies as constructor parameters
- Companion object: Contains
ZLayer.derivefor automatic dependency wiring
Example
// 1. Trait defining the service interface
trait UpdateSyncService {
def createSync(
project: Project,
integrationId: UUID,
source: Source,
destination: Destination,
transformation: Option[Spec],
): IO[StrataError, Sync]
}
// 2. Case class with all dependencies as constructor parameters
final case class UpdateSyncServiceLive(
syncRepo: SyncRepo,
syncJobScheduler: SyncJobScheduler,
integrationService: IntegrationService,
buildService: BuildService,
deploymentRepo: SyncFunctionDeploymentRepo,
) extends UpdateSyncService {
override def createSync(
project: Project,
integrationId: UUID,
source: Source,
destination: Destination,
transformation: Option[Spec],
): IO[StrataError, Sync] = {
// Implementation uses dependencies directly from constructor
for {
sync <- syncRepo.createSync(...)
_ <- syncJobScheduler.scheduleForConnection(...)
} yield sync
}
}
// 3. Companion object with ZLayer.derive, omit the type and allow the compiler to infer it
object UpdateSyncServiceLive {
val layer = ZLayer.derive[UpdateSyncServiceLive]
}
Key points:
- Dependencies are class constructor parameters, NOT accessed via
ZIO.service ZLayer.deriveautomatically inspects constructor and wires dependencies- Type signature can be explicit or inferred (both are acceptable)
Anti-Patterns
❌ Manual Layer Construction When Not Required
Bad:
object StrataJwtServiceLive {
val layer: ZLayer[JwtValidation & Config.Error, StrataJwtServiceLive] =
ZLayer.fromZIO(
for {
config <- ZIO.config[StrataJwtServiceConfig]
jwtValidation <- ZIO.service[JwtValidation]
} yield StrataJwtServiceLive(config, jwtValidation)
)
}
Good:
object StrataJwtServiceLive {
val layer = ZLayer.derive[StrataJwtServiceLive]
}
Why it's bad:
- Manual wiring is verbose and error-prone
ZLayer.derivedoes this automatically- Changes to constructor require updating the for-comprehension
❌ Using ZIO.service or ZIO.serviceWith in Business Logic
Bad:
final case class UpdateSyncServiceLive() extends UpdateSyncService {
override def createSync(...): IO[StrataError, Sync] = {
for {
syncRepo <- ZIO.service[SyncRepo]
syncJobScheduler <- ZIO.service[SyncJobScheduler]
integrationService <- ZIO.service[IntegrationService]
sync <- syncRepo.createSync(...)
_ <- syncJobScheduler.scheduleForConnection(...)
} yield sync
}
}
Good:
final case class UpdateSyncServiceLive(
syncRepo: SyncRepo,
syncJobScheduler: SyncJobScheduler,
integrationService: IntegrationService,
) extends UpdateSyncService {
override def createSync(...): IO[StrataError, Sync] = {
for {
sync <- syncRepo.createSync(...)
_ <- syncJobScheduler.scheduleForConnection(...)
} yield sync
}
}
Why it's bad:
- Makes dependencies implicit and hard to track
- Requires adding services to the environment type of every method
- Makes testing harder (can't easily inject mocks)
- Defeats the purpose of constructor-based dependency injection
When ZIO.service IS Appropriate
Main Application Files
Use ZIO.service in main application files to compose the application:
object ConnectApiApp extends StrataAppDefault {
val app = for {
corsConfig <- ZIO.config[ApiCorsConfig]
appModeConfig <- ZIO.config[AppModeConfig]
oauthRoutes <- ZIO.serviceWith[OAuth2Routes](_.routes)
} yield routes ++ HealthCheckRoutes.routes.requestLogging(appModeConfig)
override val run = app
.flatMap(_.serve)
.provideSomeAuto(
// ... all other layers
)
}
Why this is appropriate:
- Main app is responsible for composing the entire application
- This is the one place where we wire everything together
- Dependencies are provided via
.provideSomeAuto()
Accessing Contextual Information
Use ZIO.serviceWith to access contextual information from the environment:
// Define contextual information
case class MemberAuthContext(org: Organization, project: Project, member: Member)
// Use in resolvers/handlers
def getIntegration(args: IntegrationArgs): ZIO[MemberAuthContext, GraphqlError, Option[Integration]] = {
for {
project <- ZIO.serviceWith[MemberAuthContext](_.project)
integration <- integrationService.getIntegrationById(project.id, args.integrationId)
} yield integration
}
Why this is appropriate:
MemberAuthContextis request-scoped contextual information- It's provided by middleware on a per-request basis
- It's not a "dependency" in the traditional sense—it's runtime context
- Services shouldn't require it as a constructor parameter
Rule of thumb:
- If it's request-scoped or operation-scoped context: use
ZIO.service - If it's a service/repository/component: constructor parameter
Applying the Pattern
When creating a new service:
- Define the trait with the service interface
- Create a case class implementation with all dependencies as constructor parameters
- Add companion object with
val layer = ZLayer.derive[ServiceNameLive] - Never use
ZIO.serviceorZIO.serviceWithin the implementation
When reviewing code:
- Check that service implementations use constructor parameters, not
ZIO.service - Verify layers use
ZLayer.derive, not manual construction - Confirm
ZIO.serviceis only used in main app files or for contextual information
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です