Small apps can survive a pile of route handlers that talk to the database, render JSON, and enforce business rules in the same breath. Bigger projects cannot.

On the larger commerce and specialty platforms I have led, we made a deliberate shift to 3-tier architecture: presentation, domain, and data. Not as a textbook slogan. As a survival strategy.

What was breaking before the shift

Early velocity felt great. Then the codebase grew teeth:

  • route files knew SQL details and Shopify GraphQL shapes
  • business rules lived in React components “just for now”
  • Lambdas duplicated validation because nobody owned a domain layer
  • tests required a half-network to assert a price rule
  • every feature touched six layers accidentally

That is not “pragmatism.” That is accidental architecture. Accidental architecture does not scale teams. It scales confusion.

The three tiers, in working language

1. Presentation tier

Anything that speaks to a human or an external caller:

  • Remix/React admin UIs
  • POS and checkout extensions
  • Next.js portals
  • HTTP controllers and webhook entrypoints

Presentation may validate input shapes. It should not own durable business policy. Its job is translation and experience.

2. Domain tier

The rules of the business:

  • specialty order state transitions
  • credit / pricing / entitlement decisions
  • idempotency policy
  • “what does success mean” across partial failures

Domain services and pure-ish functions live here. This is where mentoring focuses, because this is where judgment lives.

3. Data tier

Persistence and external systems as dependencies:

  • Prisma/MySQL/Postgres repositories
  • Shopify API clients
  • queue publishers
  • file/FTP adapters

Data tier executes. Domain decides. Presentation asks.

What the shift looked like in practice

We did not rewrite the world in a heroic month. We enforced boundaries on every new feature and refactored hot paths when they burned us:

  1. Controllers got thin: parse, authz context, call domain, map response
  2. Repositories stopped containing policy: no “if paid then …” inside query modules
  3. Mappers became explicit: upstream payload → domain model → Shopify input
  4. Tests split: domain tests without AWS; adapter tests with fakes; a few integration tests for the seams
  5. Docs named the tiers so juniors stopped putting Prisma calls in UI callbacks

The first weeks felt slower. Then features stopped colliding. Then onboarding shortened. Then incident debugging stopped requiring psychic powers.

Benefits that showed up in delivery numbers and nerves

Change isolation

UI redesigns stopped detonating pricing rules. Database index work stopped rewriting checkout copy.

Testability

Domain tests became fast and honest. We could teach a mid-level engineer to prove a state machine without standing up Shopify.

Parallel work

One engineer could own portal presentation while another owned domain transitions behind a stable interface. That is how 5-10 person teams actually move.

Safer deploys

When tiers are real, blast radius shrinks. You can ship a presentation fix without redeploying every worker that happens to import a god-file.

Leadership leverage

3-tier is teachable. “Where does this rule belong?” becomes a weekly question with a consistent answer. That is how architecture becomes culture instead of taste.

Anti-patterns we killed on sight

  • “temporary” business logic in the React client
  • repositories that return different shapes based on user role theater
  • webhook handlers that are 400-line novellas
  • shared utils.ts that secretly became the fourth tier of doom
  • copying domain rules into Shopify Functions, UI, and cron without a source of truth

Functions and edge runtimes still need constrained logic. The point is intentional duplication with a named owner, not accidental drift.

How this pairs with Docker and services

3-tier is an interior design choice. Docker packages the deployable. Microservices are an exterior boundary choice. You can do 3-tier inside a modular monolith first. Many of our best wins started there, then split services when pain demanded it.

If you jump to microservices without tier discipline, you invent a distributed ball of mud. Congratulations: now every bad dependency requires a network hop.

Leadership takeaway

Bigger projects do not need more cleverness. They need places for rules to live.

Presentation for experience. Domain for judgment. Data for persistence and adapters. Enforce it in review. Teach it weekly. Refactor toward it when incidents point at smeared logic.

That shift did not make our systems boring. It made them changeable. Changeable systems are how teams grow without fear.

← Back to blog