Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save asfktz/49e2ca5030cb60be83da5386a6c07bbf to your computer and use it in GitHub Desktop.
Save asfktz/49e2ca5030cb60be83da5386a6c07bbf to your computer and use it in GitHub Desktop.
Effect Layers: Moving Dependencies from Service to Effect Execution

Effect Layers: Moving Dependencies from Service to Effect Execution

The Problem: Service Dependencies vs Program Dependencies

When designing Effect services, there's a critical distinction between what the service needs to exist versus what the program needs to execute. This distinction determines whether your services can be managed at the runtime level or need to be recreated for each execution.

Before: Service-Level Dependency

export class UserSrv extends Effect.Service<UserSrv>()("UserSrv", {
  dependencies: [UserRepo.Default],
  effect: Effect.gen(function* () {
    const userRepo = yield* UserRepo;
    const currentUserId = yield* CurrentUserId; // ❌ Service depends on currentUserId
    
    return {
      getMe: Effect.gen(function* () {
        return userRepo.find(currentUserId); // Uses captured currentUserId
      }),
    };
  }),
}) {}

const runtime = ManagedRuntime.make(Layer.mergeAll(UserRepo.Default));

const program = Effect.gen(function* () {
  const repo = yield* UserSrv;
  return yield* repo.getMe;
});

const aaa = program.pipe(
  Effect.provide(UserSrv.Default), // ❌ UserSrv must be provided per execution
  Effect.provideService(CurrentUserId, "123"),
);

console.log(runtime.runSync(aaa));
console.log(runtime.runSync(aaa));

Output:

Initialized UserRepo
Initialized UserRepo  // ❌ UserRepo gets reinitialized!
{ id: "123", name: "Asaf" }
Initialized UserRepo  // ❌ Again!
{ id: "123", name: "Asaf" }

Problems:

  1. UserRepo gets reinitialized on every call (3 times total!)
  2. UserSrv cannot be part of the managed runtime because it depends on CurrentUserId
  3. Poor performance due to repeated service initialization
  4. Resource waste from recreating expensive resources

After: Effect-Level Dependency

export class UserSrv extends Effect.Service<UserSrv>()("UserSrv", {
  dependencies: [UserRepo.Default],
  effect: Effect.gen(function* () {
    const userRepo = yield* UserRepo;
    
    return {
      getMe: Effect.gen(function* () {
        const currentUserId = yield* CurrentUserId; // ✅ Effect depends on currentUserId
        return userRepo.find(currentUserId);
      }),
    };
  }),
}) {}

const runtime = ManagedRuntime.make(
  Layer.mergeAll(UserSrv.Default, UserRepo.Default), // ✅ Both services in runtime
);

const program = Effect.gen(function* () {
  const repo = yield* UserSrv;
  return yield* repo.getMe;
});

const aaa = program.pipe(Effect.provideService(CurrentUserId, "123"));

console.log(runtime.runSync(aaa));
console.log(runtime.runSync(aaa));

Output:

Initialized UserRepo  // ✅ Only initialized once!
{ id: "123", name: "Asaf" }
{ id: "123", name: "Asaf" }

Benefits:

  1. UserRepo initializes only once (managed at runtime level)
  2. UserSrv is part of the managed runtime and doesn't need per-execution provision
  3. Better performance - no repeated initialization
  4. Cleaner separation between service structure and program requirements

The Key Insight

By moving currentUserId from the root of UserSrv into the getMe Effect, we made it a requirement of the program itself—not a UserSrv requirement.

This architectural change allows:

  • UserSrv to be moved to the managed runtime layer
  • Avoiding reinitializing expensive resources like database connections, HTTP clients, etc.
  • Clear separation between what the service needs to exist vs. what the program needs to execute

When to Apply This Pattern

Use this pattern when:

  • ✅ Your service has expensive initialization (database connections, file handles, etc.)
  • ✅ The dependency is program-specific rather than service-specific
  • ✅ The dependency changes between program executions (user IDs, request contexts, etc.)
  • ✅ You want to manage services at the runtime level for better performance

Avoid this pattern when:

  • ❌ The dependency is truly required for service creation
  • ❌ The dependency is static and unchanging
  • ❌ The service is stateless and cheap to recreate

Best Practices

  1. Identify true service dependencies vs. program dependencies during design
  2. Move dynamic/contextual dependencies into the effect execution phase
  3. Keep expensive resources (connections, clients) at the service level
  4. Use ManagedRuntime for services that can be shared across program executions
  5. Test initialization behavior to ensure services aren't being recreated unnecessarily

This pattern is fundamental to building efficient Effect applications that properly leverage the layer system for resource management and performance optimization.

Additional Insights from Effect Documentation

Service Dependencies vs. Effect Requirements

From the Effect documentation, we can see this pattern used extensively in real-world services. For example, when building HTTP clients or database connections:

// ❌ Don't make request-specific data a service dependency
class ApiService extends Effect.Service<ApiService>()("ApiService", {
  dependencies: [HttpClient.Default, UserSession], // UserSession varies per request
  effect: Effect.gen(function* () {
    const client = yield* HttpClient.HttpClient
    const session = yield* UserSession // This ties the service to specific session
    
    return {
      fetchUserData: Effect.succeed(client.get(`/users/${session.userId}`))
    }
  })
}) {}

// ✅ Move request-specific dependencies into the effect
class ApiService extends Effect.Service<ApiService>()("ApiService", {
  dependencies: [HttpClient.Default], // Only truly service-level dependencies
  effect: Effect.gen(function* () {
    const client = yield* HttpClient.HttpClient
    
    return {
      fetchUserData: Effect.gen(function* () {
        const session = yield* UserSession // Get session at execution time
        return yield* client.get(`/users/${session.userId}`)
      })
    }
  })
}) {}

Layer Composition Benefits

The documentation emphasizes that services following this pattern can be composed more effectively in layers:

  • Runtime-managed services can be shared across different program executions
  • Better resource management for expensive resources like database pools
  • Cleaner layer composition without circular or temporal dependencies

Context Isolation

Effect's service system is designed around this principle: services should depend on stable, infrastructure-level resources, while programs should depend on request-specific or temporal data. This allows for better:

  • Testing isolation - mock services without mocking request contexts
  • Performance optimization - expensive service initialization happens once
  • Resource sharing - database connections, HTTP clients can be pooled at the service level

This architectural pattern aligns with Effect's design philosophy of separating infrastructure concerns (services) from business logic concerns (effects).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment