Mersi

Service Layer

Effect-based service architecture in backend/ — dependency injection, live implementations, and error handling.

The backend/ service uses Effect for its service layer. This provides type-safe dependency injection, composable error handling, and runtime-swappable implementations.

Service Pattern

Every service is split into two files:

FilePurpose
backend/src/services/foo-service.tsTag + interface definition (the "contract")
backend/src/services/foo-service-live.tsProduction implementation + Layer export

Example — CartService interface:

// cart-service.ts
export class CartService extends Context.Tag("CartService")<
  CartService,
  {
    listItems: (userId: string) => Effect.Effect<CartItem[], CartError>
    addItem: (userId: string, item: AddCartItemInput) => Effect.Effect<CartItem, CartError>
    removeItem: (userId: string, itemId: string) => Effect.Effect<void, CartError>
  }
>() {}

Live implementation:

// cart-service-live.ts
export const CartServiceLive = Layer.succeed(CartService, {
  listItems: (userId) => Effect.tryPromise(() => db.select().from(cartItems).where(...)),
  addItem: (userId, item) => Effect.tryPromise(() => db.insert(cartItems).values(item)),
  removeItem: (userId, itemId) => Effect.tryPromise(() => db.update(cartItems)...),
})

Service Directory

chat-session-service.ts
chat-session-service-live.ts
cart-service.ts
cart-service-live.ts (db + Redis)
cart-onchain-service-live.ts (Sui Move)
cart-onchain-reads.ts
cart-tools.ts (AI agent tools)
checkout-service.ts
checkout-service-live.ts
order-service.ts
order-service-live.ts
mock-product-service.ts
scraping-product-service.ts
product-service.ts
product-tools.ts (AI agent tools)
cache-service.ts (Redis)
sui-relayer.ts
indexer.ts
wallet-service.ts
ServiceResponsibility
ChatSessionServiceCreate, read, update, delete chat sessions and messages
CartService (db)DB-backed cart CRUD with Redis cache
CartOnchainServiceSui Move contract cart — build PTBs, submit via relayer
CheckoutServiceOrchestrate Crossmint order creation + cart item soft-delete
OrderServiceList/get orders with local status + optional Crossmint live status
MockProductServiceReturns hardcoded products for development
ScrapingProductServiceCalls scraping/ API and caches results in Redis
CacheServiceRedis wrapper used by cart and scraping services

PRODUCT_SERVICE and CART_SERVICE Toggles

Two environment variables in backend/src/index.ts select the implementation layer at startup:

// backend/src/index.ts
const productServiceLayer =
  env.PRODUCT_SERVICE === "scraping"
    ? ScrapingProductServiceLive.pipe(Layer.provide(CacheServiceLive))
    : MockProductServiceLive

const cartServiceLayer =
  env.CART_SERVICE === "onchain"
    ? CartOnchainServiceLive
    : CartServiceLive.pipe(Layer.provide(CacheServiceLive))
Variablemock (default)scraping
PRODUCT_SERVICEMockProductServiceLive — hardcoded product listScrapingProductServiceLive — calls SCRAPING_SERVICE_URL
Variabledb (default)onchain
CART_SERVICECartServiceLive — PostgreSQL + RedisCartOnchainServiceLive — Sui Move contract

Route to Service Flow

Routes don't instantiate services directly. The service layer is provided as an Effect Layer when routes are mounted:

// backend/src/index.ts
app.route(
  "/api/cart",
  createCartRoutes(cartServiceLayer),   // inject the selected cart impl
)

Inside the route handler:

const result = await runService(
  CartService.pipe(
    Effect.flatMap((svc) => svc.listItems(userId)),
    Effect.provide(layer),             // resolve the layer at runtime
  ),
)

runService wraps Effect.runPromise and returns Either<Left<error>, Right<value>> so the handler can map errors to HTTP status codes without try/catch.

Error Handling

Each service defines typed error classes:

export class CartItemNotFoundError {
  readonly _tag = "CartItemNotFoundError"
  constructor(readonly itemId: string) {}
}

export class CartDuplicateItemError {
  readonly _tag = "CartDuplicateItemError"
}

Route handlers map error tags to HTTP status codes:

function cartErrorToStatus(tag: string): 400 | 404 | 409 | 500 {
  if (tag === "CartItemNotFoundError") return 404
  if (tag === "CartFullError") return 400
  if (tag === "CartDuplicateItemError") return 409
  return 500
}

This keeps HTTP concerns out of the service layer entirely.

How is this guide?

On this page