service-implementation
Implement Effect services as fine-grained capabilities avoiding monolithic designs
安装与使用
请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。
查看来源 ↗使用指南技能说明
Service Implementation Skill
Design and implement Effect services as focused capabilities that compose into complete solutions.
Anti-Pattern: Monolithic Services
import { ServiceMap, Effect } from "effect"
// ❌ WRONG - Mixed concerns in one service
export class PaymentService extends ServiceMap.Service<
PaymentService,
{
readonly processPayment: Effect.Effect<void>
readonly validateWebhook: Effect.Effect<void>
readonly refund: Effect.Effect<void>
readonly sendReceipt: Effect.Effect<void> // Notification concern
readonly generateReport: Effect.Effect<void> // Reporting concern
}
>()("PaymentService") {}
Pattern: Capability-Based Services
Each service represents ONE cohesive capability:
import { ServiceMap, Effect } from "effect"
declare const Doc: unique symbol
type Doc<T extends string> = { readonly [Doc]: T }
interface HandoffResult {
readonly status: string
}
interface HandoffError {
readonly _tag: "HandoffError"
readonly message: string
}
interface WebhookPayload {
readonly signature: string
readonly data: unknown
}
interface WebhookValidationError {
readonly _tag: "WebhookValidationError"
readonly message: string
}
interface PaymentId {
readonly value: string
}
interface Cents {
readonly value: number
}
interface RefundResult {
readonly status: string
}
interface RefundError {
readonly _tag: "RefundError"
readonly message: string
}
// ✅ CORRECT - Focused capabilities
export class PaymentGateway extends ServiceMap.Service<
PaymentGateway,
{
readonly handoff: (
intent: Doc<"paymentIntents">
) => Effect.Effect<HandoffResult, HandoffError, never>
// // ▲
// // No requirements leaked
}
>()("@services/payment/PaymentGateway") {}
export class PaymentWebhookGateway extends ServiceMap.Service<
PaymentWebhookGateway,
{
readonly validateWebhook: (
payload: WebhookPayload
) => Effect.Effect<void, WebhookValidationError, never>
}
>()("@services/payment/PaymentWebhookGateway") {}
export class PaymentRefundGateway extends ServiceMap.Service<
PaymentRefundGateway,
{
readonly refund: (
paymentId: PaymentId,
amount: Cents
) => Effect.Effect<RefundResult, RefundError, never>
}
>()("@services/payment/PaymentRefundGateway") {}
Pattern: No Requirement Leakage
Service operations should never have requirements:
import { ServiceMap, Effect } from "effect"
interface QueryResult {
readonly rows: ReadonlyArray<unknown>
}
interface QueryError {
readonly _tag: "QueryError"
readonly message: string
}
// The service interface stays clean
export class Database extends ServiceMap.Service<
Database,
{
readonly query: (
sql: string
) => Effect.Effect<QueryResult, QueryError, never>
// // ▲
// // Requirements = never
}
>()("Database") {}
Dependencies are handled during layer construction, not in the service interface:
import { ServiceMap, Effect, Layer } from "effect"
declare const Database: ServiceMap.Service.Tag<
Database,
{
readonly query: (sql: string) => Effect.Effect<QueryResult, QueryError, never>
}
>
declare const Config: ServiceMap.Service.Tag<
Config,
{
readonly getConfig: Effect.Effect<{ readonly connection: string }>
}
>
declare const Logger: ServiceMap.Service.Tag<
Logger,
{
readonly log: (message: string) => Effect.Effect<void>
}
>
interface QueryResult {
readonly rows: ReadonlyArray<unknown>
}
interface QueryError {
readonly _tag: "QueryError"
readonly message: string
}
declare function executeQuery(
connection: string,
sql: string
): Effect.Effect<QueryResult, QueryError>
// Dependencies live in the layer
export const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config // Dependency
const logger = yield* Logger // Dependency
return {
query:
Effect.fn(function* (sql) {
yield* logger.log(`Executing: ${sql}`)
const { connection } = yield* config.getConfig
return executeQuery(connection, sql)
})
}
})
)
Pattern: Composing Capabilities
Different implementations support different capabilities:
import { Layer } from "effect"
declare const PaymentGateway: {
readonly handoff: (intent: any) => any
}
declare const StripeHandoffLive: Layer.Layer<any>
declare const StripeWebhookLive: Layer.Layer<any>
declare const StripeRefundLive: Layer.Layer<any>
declare function fulfillCashPayment(intent: any): any
// Cash payments: Basic handoff only
export const CashGatewayLive = Layer.succeed(
PaymentGateway,
{
handoff: (intent) => fulfillCashPayment(intent)
}
)
// Stripe: Full capability suite
export const StripeGatewayLive = Layer.mergeAll(
StripeHandoffLive, // Implements PaymentGateway
StripeWebhookLive, // Implements PaymentWebhookGateway
StripeRefundLive // Implements PaymentRefundGateway
)
Pattern: Optional Capabilities
Use Effect.serviceOption for capabilities that may not be available:
import { Effect } from "effect"
import * as O from "effect/Option";
declare const PaymentGateway: {
handoff: (intent: any) => Effect.Effect<any>
}
declare const PaymentRefundGateway: {
refund: (paymentId: any, amount: any) => Effect.Effect<any>
}
interface Order {
readonly paymentIntent: any
readonly id: string
}
declare function setupRefundPolicy(
gateway: typeof PaymentRefundGateway,
order: Order
): Effect.Effect<void>
const processPayment =
Effect.fn("processPayment")(function* (order: Order) {
const handoff = yield* PaymentGateway
const result = yield* handoff.handoff(order.paymentIntent)
// Optional capability - check if available
const refundGateway = yield* Effect.serviceOption(PaymentRefundGateway)
if (O.isSome(refundGateway)) {
yield* setupRefundPolicy(refundGateway.value, order)
}
return result
})
Testing Benefits
Each capability can be tested in isolation:
import { Effect, Layer, pipe } from "effect"
declare const PaymentWebhookGateway: ServiceMap.Service.Tag<
PaymentWebhookGateway,
{
validateWebhook: (payload: WebhookPayload) => Effect.Effect<void, WebhookValidationError>
}
>
interface WebhookPayload {
readonly signature: string
readonly data: unknown
}
interface WebhookValidationError {
readonly _tag: "WebhookValidationError"
readonly reason: string
}
declare function handleWebhook(payload: WebhookPayload): Effect.Effect<void, WebhookValidationError, any>
declare const payload: WebhookPayload
const TestWebhook = Layer.succeed(
PaymentWebhookGateway,
{
validateWebhook: (payload) =>
payload.signature === "valid"
? Effect.succeed(undefined)
: Effect.fail(new WebhookValidationError({ reason: "Invalid" }))
}
)
// Test only webhook validation, no other payment concerns
const testProgram = handleWebhook(payload).pipe(
Effect.provide(TestWebhook)
)
Naming Convention
Use descriptive capability names:
-
*Gateway- External system integration -
*Repository- Data persistence -
*Domain- Business logic -
*Service- General capability (use sparingly)
Tag identifiers should include namespace:
-
"@services/payment/PaymentGateway" -
"@repositories/user/UserRepository" -
"@domain/order/OrderDomain"
Quality Checklist
- Service represents single capability
- All operations have Requirements = never
- Tagged with descriptive namespace
- Dependencies handled in layer
- Can be tested in isolation
- Can be composed with other capabilities
- JSDoc with purpose and usage
Keep services focused, composable, and free of leaked requirements.
使用示例
查看来源仓库中的使用示例与配置说明。
查看来源 ↗来源历史
- 收录时间
- 2026-03-11
- 索引更新时间
- 2026-03-11
- Branch
- main
收录与同步日期属于本站记录。源码修改历史请查看来源仓库。
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!