| name | effect-service |
| description | Effect service definition, interface design, error types, retries. Use when creating new services or defining error hierarchies. |
| user-invocable | false |
Effect Service Design Patterns
Apply these patterns when designing Effect services in this codebase.
See /effect-layers for layer construction, composition, and provision.
Service Definition
Default: combined service class + inline interface
For services with a single implementation, define the interface inline on
the ServiceMap.Service class. This is concise and gives you full Layer-based
dependency injection and testability.
class NotificationService extends ServiceMap.Service<
NotificationService,
{
readonly send: (to: string, message: string) => Effect.Effect<void, AppError>;
readonly sendBatch: (
notifications: ReadonlyArray<Notification>,
) => Effect.Effect<void, AppError>;
}
>()("axm.sh/NotificationService") {}
The service class decouples consumers from implementations — you get testability
and composability without additional abstraction.
Appropriate for:
- Application-specific coordination or orchestration services
- Domain services with a single production implementation
- Config or environment wrappers
- Any service where you don't anticipate swapping implementations
When to extract an explicit interface
Use a separate interface when a service genuinely has multiple
implementations that must conform to a shared contract.
interface DocumentStore {
readonly get: (id: DocumentId) => Effect.Effect<Document, DocumentNotFound>;
readonly put: (doc: Document) => Effect.Effect<void, StoreError>;
readonly delete: (id: DocumentId) => Effect.Effect<void, StoreError>;
}
const DocumentStore = ServiceMap.Service<DocumentStore>("axm.sh/DocumentStore");
const S3DocumentStoreLayer = Layer.succeed(DocumentStore, {});
const PostgresDocumentStoreLayer = Layer.succeed(DocumentStore, {});
This pattern is warranted when:
- Multiple implementations exist today — different backends, provider
clients, or strategy-pattern services selected at runtime
- Test doubles need a formal contract — complex test fakes validated
against the same shape to prevent drift
- The interface is a domain port — adapter boundary where the domain
defines what it needs and infrastructure satisfies it
Decision rule
Ask: does this service have, or will it concretely have, more than one
implementation?
- No → Combined service class pattern. Extracting an interface later is a
straightforward, non-breaking refactor.
- Yes → Explicit interface with
ServiceMap.Service<Shape>(id).
Avoid speculative interfaces. The Layer system makes it cheap to introduce
one later, so let actual requirements drive the decision.
Service Interface Design
R = never on methods
Service methods must not leak implementation dependencies. Dependencies
belong in the layer, not the service interface.
interface MyService {
readonly query: (sql: string) => Effect.Effect<Row[], Error, Config | Logger>;
}
interface MyService {
readonly query: (sql: string) => Effect.Effect<Row[], Error>;
}
See /effect-layers for how to capture dependencies in layers while keeping
R = never.
Readonly properties
Services should not expose mutable state. Use readonly on all properties.
interface CounterService {
readonly increment: () => Effect.Effect<void>;
readonly get: () => Effect.Effect<number>;
}
Single responsibility
Each service handles one domain concern. If a service has methods spanning
multiple concerns, split it.
Service-Driven Development
Design leaf service interfaces before implementations. This lets you model
higher-level orchestration that type-checks immediately.
class Users extends ServiceMap.Service<
Users,
{ readonly findById: (id: UserId) => Effect.Effect<User, UserNotFound> }
>()("@app/Users") {}
class Tickets extends ServiceMap.Service<
Tickets,
{ readonly issue: (eventId: EventId, userId: UserId) => Effect.Effect<Ticket> }
>()("@app/Tickets") {}
class Events extends ServiceMap.Service<
Events,
{ readonly register: (eventId: EventId, userId: UserId) => Effect.Effect<Registration> }
>()("@app/Events") {
static readonly layer = Layer.effect(
Events,
Effect.gen(function* () {
const users = yield* Users;
const tickets = yield* Tickets;
return Events.of({
register: (eventId, userId) =>
Effect.gen(function* () {
const user = yield* users.findById(userId);
const ticket = yield* tickets.issue(eventId, userId);
return { user, ticket };
}),
});
}),
);
}
Benefits:
- Type-checks immediately even without leaf implementations
- Adding production layers doesn't change orchestration code
- Test layers slot in without modification
Naming Conventions
Service identifiers
Use axm.sh/<ServiceName> as the identifier string:
ServiceMap.Service<Workspace, {}>()("axm.sh/Workspace");
ServiceMap.Service<SourceHostProviders, {}>()("axm.sh/SourceHostProviders");
Layer names
See /effect-layers for full naming conventions and module structure.
| Suffix | Usage |
|---|
layer | Production layer |
layerTest | Test / fake layer |
layerMemory | In-memory variant |
layerDev | Development / local variant |
Using Services
Yield the service to access it in an effect:
const program = Effect.gen(function* () {
const ws = yield* Workspace;
const sources = yield* SourceHostProviders;
const refs = yield* sources.find(source);
});
The R parameter automatically tracks required services as a union type.
Error Type Design
Use Data.TaggedError for pattern matching and structural equality:
export class ApiError extends Data.TaggedError("ApiError")<{
readonly status?: number;
readonly message: string;
readonly retryable: boolean;
}> {}
Map external errors at boundaries:
const mapApiError = (error: unknown): ApiError => {
if (error instanceof ExternalApiError) {
return new ApiError({
status: error.status,
message: error.message,
retryable: error.status === 429 || error.status >= 500,
});
}
return new ApiError({
message: `Unexpected error: ${String(error)}`,
retryable: false,
});
};
Error Type Checklist
Retry Policies
Define retry behavior using Schedule:
const retryPolicy = Schedule.exponential(Duration.seconds(1)).pipe(
Schedule.intersect(Schedule.recurs(3)),
Schedule.whileInput((error: ApiError) => error.retryable),
);
const doSomething = (input: string) =>
Effect.tryPromise({
try: () => api.call(input),
catch: mapApiError,
}).pipe(Effect.retry(retryPolicy));
Retry Policy Checklist
Service Design Checklist