← All engineering notes
BACKEND·8 min read

Designing a Backend That Adapts to 67 Business Types

A pharmacy and a hotel need the same booking primitive and almost nothing else in common. Here's how we let a business's type reshape the product without a fork in the codebase.

Omari serves salons, pharmacies, hotels, and dozens of other business types from one platform. Instead of a table — or a codebase — per vertical, every business carries a type, and that type is a first-class input to the domain layer: it decides which fields, workflows, and screens are active, not a conditional buried in a controller.

We modeled the platform as a set of independent capabilities — bookings, prescriptions, room inventory, loyalty, messaging — that a business type opts into. A pharmacy composes prescription handling with delivery; a hotel composes room inventory with housekeeping schedules. Neither module knows the other exists.

domain/Capabilities.kt
enum class BusinessType { SALON, PHARMACY, HOTEL }

sealed interface Capability
object Bookings : Capability
object Prescriptions : Capability
object RoomInventory : Capability

val CAPABILITIES_BY_TYPE: Map<BusinessType, Set<Capability>> = mapOf(
    BusinessType.PHARMACY to setOf(Bookings, Prescriptions),
    BusinessType.HOTEL to setOf(Bookings, RoomInventory),
)

fun Business.hasCapability(c: Capability) =
    CAPABILITIES_BY_TYPE[type]?.contains(c) == true

The 67-types problem tempts you toward per-client forks — fast to ship, brutal to maintain once the tenth fork needs the same security patch. Composable capabilities keep the maintenance surface to one codebase, at the cost of more upfront design discipline.

Model capability as data, not code branches. Every feature module checks "business.hasCapability(X)" — never "if (business.type == PHARMACY)". Adding a new business type becomes a row in a table, not a new conditional scattered across the codebase.

When "it depends on the business" is the actual requirement, model that dependency explicitly in the domain layer — don't leave it to scattered conditionals or a fleet of near-identical forks.