Implementing Business Logic

Using Worker and Observer

This document explains how to write and use domain-level business logic in Baselines using two core primitives:

  • Worker — for one-time business operations
  • Observer — for continuous or reactive business state

If you are implementing custom business logic in the domain layer, this is the entry point.


Why Worker and Observer Exist

Baselines enforces a strict separation between UI, domain logic, and data access.

Business logic must:

  • be explicit
  • be testable
  • fail safely
  • not depend on UI or platform code

Worker and Observer provide a standardized way to express business intent while enforcing these rules.


When to Use Which

ScenarioUse
Fetch data onceWorker
Submit or mutate dataWorker
Trigger an actionWorker
Observe state over timeObserver
React to data changesObserver
Stream updates to UIObserver

Writing a Worker

A Worker represents a single business operation.

It:

  • runs once
  • returns a Result
  • captures and logs non-cancellation failures
  • propagates coroutine cancellation to the caller

Exception Handling and Cancellation

Non-cancellation exceptions thrown by doWork are logged and returned as Result.failure(cause). Handle them with onFailure { ... }, or use getOrThrow() when the caller should propagate the failure.

CancellationException, including TimeoutCancellationException, is rethrown. It is not logged as a worker failure or delivered to the caller's Result.onFailure. This preserves cancellation when a ViewModel is cleared or a parent coroutine is cancelled.

If a timeout represents a business failure, translate that specific timeout at the operation that owns it. Any additional runCatching or broad exception handler around a worker must also rethrow cancellation.

Example

kotlin
1// Example domain worker; UserRepository and User are your domain contracts.
2@Inject
3class FetchUser(
4 private val repository: UserRepository,
5) : Worker<String, User>() {
6
7 override suspend fun doWork(params: String): User {
8 return repository.getUserById(params)
9 }
10}
11
12// In a ViewModel coroutine or another suspend caller:
13fetchUser(userId)
14 .onSuccess { user -> /* Update UI state */ }
15 .onFailure { cause -> /* Show a recoverable error */ }

Worker does not switch dispatchers. Use injected AppDispatchers from io.baselines.toolkit.concurrency when an operation needs a particular execution context.

Writing an Observer

An Observer returns a Flow from create(params):

kotlin
1@Inject
2class ObserveUser(
3 private val repository: UserRepository,
4) : Observer<String, User>() {
5
6 override suspend fun create(params: String): Flow<User> {
7 return repository.observeUserById(params)
8 }
9}

The repository methods above are illustrative. Unlike Worker, Observer does not wrap execution in Result or install an error handler. Handle failures where the flow is produced or collected, and let cancellation propagate.