Logic Components

This section is currently being developed. Check back soon for comprehensive documentation on logic components.

Logic components are pieces of software that contain the business logic of your application. They are organized semantically by domain or functionality and serve as the orchestration layer between controllers and adapters.

Purpose

Logic components:

  • Implement business rules and domain logic
  • Orchestrate operations across multiple adapters
  • Encapsulate complex workflows
  • Remain independent of transport layer (HTTP, WebSocket) concerns
  • Can be thoroughly unit tested without I/O dependencies

Location

Logic components SHOULD be placed under the src/logic or src/components directory, organized by domain:

src/
└── logic/
    ├── users/
    ├── content/
    ├── permissions/
    └── notifications/

Characteristics

1. Adapter Consumption

Logic components are the primary consumers of adapters. They use adapters to interact with external resources while focusing on business rules.

export function createUserLogic(
  components: Pick<AppComponents, 'database' | 'cache' | 'logs'>
): IUserLogic {
  const { database, cache, logs } = components
  const logger = logs.getLogger('user-logic')

  async function getUserProfile(userId: string): Promise<UserProfile> {
    // Check cache first
    const cached = await cache.get(`user:${userId}`)
    if (cached) {
      return cached
    }

    // Fetch from database
    const user = await database.query('SELECT * FROM users WHERE id = $1', [userId])
    
    // Apply business logic
    const profile = transformUserToProfile(user)
    
    // Cache the result
    await cache.set(`user:${userId}`, profile, { ttl: 3600 })
    
    return profile
  }

  return {
    getUserProfile
  }
}

2. Business Rules

Logic components enforce business rules and validation:

async function createUser(userData: CreateUserInput): Promise<User> {
  // Business rule: username must be unique
  const existing = await database.query(
    'SELECT id FROM users WHERE username = $1',
    [userData.username]
  )
  
  if (existing.rows.length > 0) {
    throw new UserAlreadyExistsError(userData.username)
  }

  // Business rule: validate user data
  if (!isValidEmail(userData.email)) {
    throw new InvalidEmailError(userData.email)
  }

  // Create the user
  const user = await database.query(
    'INSERT INTO users (username, email) VALUES ($1, $2) RETURNING *',
    [userData.username, userData.email]
  )

  logger.info('User created', { userId: user.id })
  
  return user
}

3. Workflow Orchestration

Logic components coordinate complex multi-step operations:

async function publishContent(
  userId: string,
  content: ContentInput
): Promise<PublishedContent> {
  // Step 1: Validate permissions
  const canPublish = await permissions.canUserPublish(userId)
  if (!canPublish) {
    throw new UnauthorizedError('User cannot publish content')
  }

  // Step 2: Process content
  const processed = await processContent(content)

  // Step 3: Store content
  const stored = await storage.save(processed)

  // Step 4: Update indexes
  await searchIndex.index(stored)

  // Step 5: Notify subscribers
  await notifications.notifySubscribers(userId, stored)

  logger.info('Content published', { contentId: stored.id, userId })

  return stored
}

Best Practices

1. Single Responsibility

Each logic component should focus on one domain or bounded context:

// Good: Focused on user domain
createUserLogic()
createContentLogic()
createPermissionsLogic()

// Avoid: Mixed responsibilities
createUserAndContentLogic()

2. Dependency Injection

Always inject dependencies through the components parameter:

export function createOrderLogic(
  components: Pick<AppComponents, 'database' | 'payments' | 'inventory' | 'logs'>
): IOrderLogic {
  // Use injected dependencies
}

3. Error Handling

Throw meaningful domain errors that controllers can catch and handle:

// Define domain-specific errors
export class InsufficientInventoryError extends Error {
  constructor(public readonly productId: string, public readonly requested: number) {
    super(`Insufficient inventory for product ${productId}. Requested: ${requested}`)
    this.name = 'InsufficientInventoryError'
  }
}

4. Pure Business Logic

Keep logic components free from transport layer concerns:

// Good: Pure business logic
async function calculateOrderTotal(items: OrderItem[]): Promise<number> {
  return items.reduce((total, item) => total + item.price * item.quantity, 0)
}

// Avoid: HTTP/transport layer concerns
async function calculateOrderTotal(req: Request, res: Response): Promise<void> {
  // Don't do this in logic components
}

Testing Logic Components

Logic components should be thoroughly unit tested. See the Testing Services (WKC) documentation for details.

describe('when creating a user', () => {
  let userLogic: IUserLogic
  let mockDatabase: jest.Mocked<IDatabase>

  beforeEach(() => {
    mockDatabase = createMockDatabase()
    userLogic = createUserLogic({ database: mockDatabase, logs: mockLogs })
  })

  describe('and the username already exists', () => {
    beforeEach(() => {
      mockDatabase.query.mockResolvedValueOnce({ rows: [{ id: '123' }] })
    })

    it('should throw UserAlreadyExistsError', async () => {
      await expect(
        userLogic.createUser({ username: 'existing', email: 'test@test.com' })
      ).rejects.toThrow(UserAlreadyExistsError)
    })
  })
})

Coming Soon

This section will be expanded with:

  • Detailed examples of domain-driven logic components
  • Patterns for handling complex workflows
  • Guidelines for organizing large domain logic
  • Integration with event systems
  • Caching strategies
  • Transaction management patterns