EF

effect-patterns-domain-modeling

This provides best practices for using the Effect-TS library to create type-safe errors and domain logic.

Install

mkdir -p .claude/skills/effect-patterns-domain-modeling && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/4266" && unzip -o skill.zip -d .claude/skills/effect-patterns-domain-modeling && rm skill.zip

Installs to .claude/skills/effect-patterns-domain-modeling

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Effect-TS patterns for Domain Modeling. Use when working with domain modeling in Effect-TS applications.
104 chars✓ has a “when” trigger
Advanced

Key capabilities

  • Defines type-safe tagged domain errors
  • Models business logic using Effect data types
  • Implements validation pipelines
  • Structures domain-specific result patterns
  • Encapsulates success and failure paths

How it works

It utilizes Data.TaggedError to create a discriminated union of domain errors, ensuring exhaustive handling by the compiler.

Inputs & outputs

You give it
Domain logic requirements
You get back
Typed Effect-based implementation code

When to use effect-patterns-domain-modeling

  • Create type-safe domain errors
  • Refactor domain models with Effect
  • Implement robust validation logic

About this skill

Effect-TS Patterns: Domain Modeling

This skill provides 15 curated Effect-TS patterns for domain modeling. Use this skill when working on tasks related to:

  • domain modeling
  • Best practices in Effect-TS applications
  • Real-world patterns and solutions

🟢 Beginner Patterns

Create Type-Safe Errors

Rule: Use Data.TaggedError to create typed, distinguishable errors for your domain.

Good Example:

import { Effect, Data } from "effect"

// ============================================
// 1. Define tagged errors for your domain
// ============================================

class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
  readonly userId: string
}> {}

class InvalidEmailError extends Data.TaggedError("InvalidEmailError")<{
  readonly email: string
  readonly reason: string
}> {}

class DuplicateUserError extends Data.TaggedError("DuplicateUserError")<{
  readonly email: string
}> {}

// ============================================
// 2. Use in Effect functions
// ============================================

interface User {
  id: string
  email: string
  name: string
}

const validateEmail = (email: string): Effect.Effect<string, InvalidEmailError> => {
  if (!email.includes("@")) {
    return Effect.fail(new InvalidEmailError({
      email,
      reason: "Missing @ symbol"
    }))
  }
  return Effect.succeed(email)
}

const findUser = (id: string): Effect.Effect<User, UserNotFoundError> => {
  // Simulate database lookup
  if (id === "123") {
    return Effect.succeed({ id, email: "[email protected]", name: "Alice" })
  }
  return Effect.fail(new UserNotFoundError({ userId: id }))
}

const createUser = (
  email: string,
  name: string
): Effect.Effect<User, InvalidEmailError | DuplicateUserError> =>
  Effect.gen(function* () {
    const validEmail = yield* validateEmail(email)

    // Simulate duplicate check
    if (validEmail === "[email protected]") {
      return yield* Effect.fail(new DuplicateUserError({ email: validEmail }))
    }

    return {
      id: crypto.randomUUID(),
      email: validEmail,
      name,
    }
  })

// ============================================
// 3. Handle errors by tag
// ============================================

const program = createUser("[email protected]", "Alice").pipe(
  Effect.catchTag("InvalidEmailError", (error) =>
    Effect.succeed({
      id: "fallback",
      email: "[email protected]",
      name: `${error.email} was invalid: ${error.reason}`,
    })
  ),
  Effect.catchTag("DuplicateUserError", (error) =>
    Effect.fail(new Error(`Email ${error.email} already registered`))
  )
)

// ============================================
// 4. Match on all errors
// ============================================

const handleAllErrors = createUser("bad-email", "Bob").pipe(
  Effect.catchTags({
    InvalidEmailError: (e) => Effect.succeed(`Invalid: ${e.reason}`),
    DuplicateUserError: (e) => Effect.succeed(`Duplicate: ${e.email}`),
  })
)

// ============================================
// 5. Run and see results
// ============================================

Effect.runPromise(program)
  .then((user) => console.log("Created:", user))
  .catch((error) => console.error("Failed:", error))

Rationale:

Create domain-specific errors using Data.TaggedError. Each error type gets a unique _tag for pattern matching.


Plain Error or string messages cause problems:

  1. No type safety - Can't know what errors a function might throw
  2. Hard to handle - Matching on error messages is fragile
  3. Poor documentation - Errors aren't part of the function signature

Tagged errors solve this by making errors typed and distinguishable.



Handle Missing Values with Option

Rule: Use Option instead of null/undefined to make missing values explicit and type-safe.

Good Example:

import { Option, Effect } from "effect"

// ============================================
// 1. Creating Options
// ============================================

// Some - a value is present
const hasValue = Option.some(42)

// None - no value
const noValue = Option.none<number>()

// From nullable - null/undefined becomes None
const fromNull = Option.fromNullable(null)        // None
const fromValue = Option.fromNullable("hello")    // Some("hello")

// ============================================
// 2. Checking and extracting values
// ============================================

const maybeUser = Option.some({ name: "Alice", age: 30 })

// Check if value exists
if (Option.isSome(maybeUser)) {
  console.log(`User: ${maybeUser.value.name}`)
}

// Get with default
const name = Option.getOrElse(
  Option.map(maybeUser, u => u.name),
  () => "Anonymous"
)

// ============================================
// 3. Transforming Options
// ============================================

const maybeNumber = Option.some(5)

// Map - transform the value if present
const doubled = Option.map(maybeNumber, n => n * 2)  // Some(10)

// FlatMap - chain operations that return Option
const safeDivide = (a: number, b: number): Option.Option<number> =>
  b === 0 ? Option.none() : Option.some(a / b)

const result = Option.flatMap(maybeNumber, n => safeDivide(10, n))  // Some(2)

// ============================================
// 4. Domain modeling example
// ============================================

interface User {
  readonly id: string
  readonly name: string
  readonly email: Option.Option<string>  // Email is optional
  readonly phone: Option.Option<string>  // Phone is optional
}

const createUser = (name: string): User => ({
  id: crypto.randomUUID(),
  name,
  email: Option.none(),
  phone: Option.none(),
})

const addEmail = (user: User, email: string): User => ({
  ...user,
  email: Option.some(email),
})

const getContactInfo = (user: User): string => {
  const email = Option.getOrElse(user.email, () => "no email")
  const phone = Option.getOrElse(user.phone, () => "no phone")
  return `${user.name}: ${email}, ${phone}`
}

// ============================================
// 5. Use in Effects
// ============================================

const findUser = (id: string): Effect.Effect<Option.Option<User>> =>
  Effect.succeed(
    id === "123"
      ? Option.some({ id, name: "Alice", email: Option.none(), phone: Option.none() })
      : Option.none()
  )

const program = Effect.gen(function* () {
  const maybeUser = yield* findUser("123")

  if (Option.isSome(maybeUser)) {
    yield* Effect.log(`Found: ${maybeUser.value.name}`)
  } else {
    yield* Effect.log("User not found")
  }
})

Effect.runPromise(program)

Rationale:

Use Option<A> to represent values that might be absent. This makes "might not exist" explicit in your types, forcing you to handle both cases.


null and undefined cause bugs because:

  1. Silent failures - Accessing .property on null crashes at runtime
  2. Unclear intent - Is null "not found" or "error"?
  3. Forgotten checks - Easy to forget if (x !== null)

Option fixes this by making absence explicit and type-checked.



Your First Domain Model

Rule: Start domain modeling by defining clear interfaces for your business entities.

Good Example:

import { Effect } from "effect"

// ============================================
// 1. Define domain entities as interfaces
// ============================================

interface User {
  readonly id: string
  readonly email: string
  readonly name: string
  readonly createdAt: Date
}

interface Product {
  readonly sku: string
  readonly name: string
  readonly price: number
  readonly inStock: boolean
}

interface Order {
  readonly id: string
  readonly userId: string
  readonly items: ReadonlyArray<OrderItem>
  readonly total: number
  readonly status: OrderStatus
}

interface OrderItem {
  readonly productSku: string
  readonly quantity: number
  readonly unitPrice: number
}

type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered"

// ============================================
// 2. Create domain functions
// ============================================

const createUser = (email: string, name: string): User => ({
  id: crypto.randomUUID(),
  email,
  name,
  createdAt: new Date(),
})

const calculateOrderTotal = (items: ReadonlyArray<OrderItem>): number =>
  items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0)

// ============================================
// 3. Use in Effect programs
// ============================================

const program = Effect.gen(function* () {
  const user = createUser("[email protected]", "Alice")
  yield* Effect.log(`Created user: ${user.name}`)

  const items: OrderItem[] = [
    { productSku: "WIDGET-001", quantity: 2, unitPrice: 29.99 },
    { productSku: "GADGET-002", quantity: 1, unitPrice: 49.99 },
  ]

  const order: Order = {
    id: crypto.randomUUID(),
    userId: user.id,
    items,
    total: calculateOrderTotal(items),
    status: "pending",
  }

  yield* Effect.log(`Order total: $${order.total.toFixed(2)}`)
  return order
})

Effect.runPromise(program)

Rationale:

Start by defining TypeScript interfaces that represent your business entities. Use descriptive names that match your domain language.


Good domain modeling:

  1. Clarifies intent - Types document what data means
  2. Prevents errors - Compiler catches wrong data usage
  3. Enables tooling - IDE autocompletion and refactoring
  4. Communicates - Code becomes documentation


🟡 Intermediate Patterns

Model Optional Values Safely with Option

Rule: Use Option<A> to explicitly model values that may be absent, avoiding null or undefined.

Good Example:

A function that looks for a user in a database is a classic use case. It might find a user, or it might not. Returning an Option<User> makes this contract explicit and safe.

import { Effect, Option } from "effect";


---

*Content truncated.*

When not to use it

  • Simple scripts lacking domain complexity
  • Libraries already providing rigid error handling interfaces

Prerequisites

Effect-TS library

Limitations

  • Steep learning curve for TypeScript developers
  • Boilerplate heavy for simple entity modeling

How it compares

It shifts error handling from runtime checks to static, type-safe definitions specific to business entities.

Compared to similar skills

effect-patterns-domain-modeling side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
effect-patterns-domain-modeling (this skill)17moReviewAdvanced
react-component-patterns31moNo flagsIntermediate
typescript-pro34moNo flagsAdvanced
nx-generate16moReviewBeginner

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry