Skip to main content

RFC-008: Responsibility Segregation and Feature-First Architecture

Status

Proposed

Overview

This RFC defines a new code ownership model for the Photobooth Web App. The current repository structure is organized mostly by framework primitives such as app, providers, components, store, and lib. That structure is no longer providing clear boundaries as the application grows.

The target architecture is feature-first with thin shared layers:

  • app/ for route composition only
  • features/ for workflow logic, hooks, and feature-owned UI
  • ui/ for shared presentational components
  • domain/ for pure business logic and models
  • infra/ for storage, worker, device, and network adapters
  • server/ for transport, orchestration, business logic, and IO boundaries on the backend

The intent is to reduce coordination cost, stop new behavior from accumulating in lib, and make the largest parts of the app testable without page-level coupling.

Problem Statement

The repository currently has nominal layers, but responsibilities leak across them:

  • src/app pages own routing, workflow state, side effects, API calls, and persistence orchestration
  • src/providers combine React context with browser APIs, device interactions, and feature logic
  • src/lib mixes pure utilities, browser-only code, storage, image processing, worker adapters, and API helpers
  • src/store is coupled to UI workflows and persistence format
  • src/server has better boundaries than the frontend, but some controllers and use-cases still mix transport concerns with domain logic

This has already created complexity hotspots:

  • src/app/customization/components/tabs/frame/visual-frame-editor.tsx
  • src/app/customization/page.tsx
  • src/app/customization/components/live-preview.tsx
  • src/lib/image-processing.ts
  • src/lib/storage.ts
  • src/app/image-upload/page.tsx
  • src/providers/printer-provider.tsx

The issue is not only file size. The main problem is that a single file often owns multiple reasons to change: rendering, mutation logic, side effects, browser/device access, and domain rules.

Current Architecture

The current runtime diagram is accurate at a system level, but it does not describe code ownership boundaries well enough for refactoring.

Current Layering Diagram

Current Responsibility Summary

  • Pages act as route components and feature controllers at the same time
  • Providers act as state containers and integration layers at the same time
  • lib acts as a shared utility folder and a domain owner at the same time
  • Domain logic is spread across UI files, providers, stores, and utilities

Target Architecture

The target architecture standardizes feature-first ownership with narrow shared horizontal layers.

Target Layering Diagram

Dependency Direction

  • app may depend on features, ui, and route-level framework primitives
  • features may depend on ui, domain, and infra
  • ui may depend on other ui modules and minimal shared utilities
  • domain may depend only on other pure domain modules
  • infra may depend on domain and external APIs, but must not own business policy
  • backend controllers may depend on use-cases and transport helpers only

Layer Responsibilities

app/

Allowed:

  • route definitions
  • page composition
  • layout composition
  • dynamic imports and route-specific assembly

Not allowed:

  • business workflows
  • storage orchestration
  • direct device/browser integration
  • non-trivial API/data orchestration

Rule:

  • pages should compose feature entrypoints and remain mostly declarative

features/

Allowed:

  • feature-specific hooks
  • workflow orchestration
  • feature-owned components
  • feature-specific API call coordination
  • state transitions for one feature area

Not allowed:

  • generic shared UI primitives
  • cross-feature dumping of unrelated helpers
  • direct ownership of persistence or transport implementations

Rule:

  • feature modules own behavior, not infrastructure

ui/

Allowed:

  • shared visual primitives
  • styling
  • presentational composition

Not allowed:

  • fetch
  • storage access
  • device APIs
  • business rules

Rule:

  • shared UI renders data provided from above and emits user intent upward

domain/

Allowed:

  • pure business rules
  • data transformations
  • frame/layout/image math
  • validation and state transitions that do not require IO

Not allowed:

  • React
  • browser APIs
  • fetch/network code
  • IndexedDB/localStorage/device access

Rule:

  • domain code must be pure and easy to unit test

infra/

Allowed:

  • persistence adapters
  • worker adapters
  • printer/device adapters
  • API URL building
  • external service wrappers

Not allowed:

  • business policy that changes based on product rules
  • rendering concerns

Rule:

  • infrastructure modules talk to external systems through explicit interfaces

server/

Allowed:

  • controllers for request/response handling
  • use-cases for orchestration
  • services or domain modules for business rules
  • repositories/outbound clients for IO

Not allowed:

  • controllers owning business policy
  • use-cases formatting transport responses directly when a serializer/helper should do it

Rule:

  • server controller code should remain transport-only

Mapping Current Layers to Target Layers

Current locationCurrent problemTarget location
src/app/customization/*page and component files own workflow logicsrc/features/customization/* plus small route composition in src/app
src/lib/image-processing.tspure transforms, canvas work, and worker coordination are mixedsrc/domain/image-processing/* and src/infra/image-processing/*
src/lib/storage.tstransport, fallback policy, and migration logic are mixedsrc/infra/persistence/*
src/providers/printer-provider.tsxprovider owns device logic and workflow statesrc/features/printing/* and src/infra/printing/*
src/providers/license-provider.tsxprovider mixes feature behavior with API callssrc/features/license/* and shared API adapters in src/infra/*
src/server/sharing/*controller owns too much request-specific behaviorthinner controller/use-case/service boundaries in src/server/*
src/server/payments/*orchestration and transport concerns are close togetherkeep use-case and controller boundaries explicit

Initial Migration Plan

Migration is incremental and behavior-preserving. This RFC does not authorize a large rewrite.

Track 1: Customization and Editor Decomposition

Introduce:

  • src/features/customization/editor/*
  • src/features/customization/hooks/*

First extractions:

  • selection and drag/resize logic from the visual editor
  • preset CRUD logic from the customization page
  • customer license lookup from the customization page
  • feature-level orchestration from page components

Temporary state:

  • routes may continue to import feature modules from src/app during migration
  • existing shared UI can remain in place until its ownership becomes clear

Track 2: Image Processing Split

Introduce:

  • src/domain/image-processing/*
  • src/infra/image-processing/*

First extractions:

  • color adjustment and dithering logic into pure domain modules
  • crop/layout math into pure domain modules
  • worker integration and browser canvas entrypoints into infra modules

Temporary state:

  • one compatibility facade may remain while call sites migrate away from src/lib/image-processing.ts

Track 3: Storage and Persistence Split

Introduce:

  • src/infra/persistence/*

First extractions:

  • IndexedDB adapter
  • localStorage adapter
  • reconciliation/dual-write policy
  • settings shape migration helpers

Temporary state:

  • stores and providers may continue to consume a facade while the persistence boundary is formalized

File and Complexity Guardrails

Use these working limits going forward:

  • page or component files should target under 200 LOC
  • page or component files above 300 LOC require decomposition or explicit justification
  • domain and utility modules should target under 150-200 LOC unless algorithmically dense
  • a single file should not combine rendering, side effects, and business rules
  • new complex logic should not be added to src/lib without a documented reason

These are review guardrails, not hard build rules.

Acceptance Criteria

This RFC is considered successful when the following become true over the next refactor phases:

  • new feature work lands in features/* rather than app pages or lib
  • route files in app become composition-only
  • the customization/editor surface is split into smaller workflow and rendering modules
  • image-processing is separated into pure logic and adapter layers
  • storage is separated into explicit persistence adapters and migration helpers
  • server controllers become thinner where transport and business logic currently mix

Non-Goals

  • no visual redesign
  • no intentional product behavior changes
  • no mandatory one-shot rename of the entire repository
  • no backend rewrite beyond boundary cleanup
  • no attempt to fully eliminate all existing top-level folders immediately

Testing and Verification

Each migration slice should preserve behavior and move tests with the extracted logic.

  • pure domain logic should gain direct unit tests
  • feature hooks should gain focused tests where page-level coupling exists today
  • shared UI should be testable without feature context where practical
  • API behavior should remain stable while server boundary cleanup happens
  • each slice should be deployable independently

Default Decisions

This RFC makes these defaults explicit:

  • architecture direction: feature-first
  • lib is no longer the default home for complex code
  • runtime boundaries should be explicit rather than hidden behind convenience layers
  • the first implementation slice after the RFC should be the customization/editor area