Skip to content

Clean Architecture ​

Gatherloop POS is built on Clean Architecture principles — both on the Go backend and the shared TypeScript frontend — so that business rules can be written and tested without a database, a browser, or a phone in the loop. Each side separates into three layers: Domain (what the business does), Data (where it gets its facts), and Presentation (how it's exposed or shown). Dependencies only ever point inward, toward Domain.

Backend ​

Clean Architecture — backend

Domain layer ​

The core of the backend. It defines the data structures and every business rule, with no knowledge of HTTP or SQL.

  • Entity — plain Go structs defining the data, e.g. Transaction.
  • Usecase — the actual business logic (validating a transaction, deducting a wallet balance, recomputing a budget), e.g. TransactionUsecase.
  • Repository interface — the contract a data source must satisfy, e.g. TransactionRepository. The usecase depends on this interface, never on MySQL directly — which is what makes it possible to unit-test business logic against a mock repository instead of a real database.

Data layer ​

Implements the repository interfaces the domain layer requires, translating between SQL rows and domain entities. Because the domain only knows the interface, this implementation could be swapped for a different database without touching a single business rule.

Presentation layer ​

HTTP handlers that parse incoming requests into domain types, call the usecase, and transform the result back into JSON. This is the only layer that knows HTTP exists.

Frontend ​

Clean Architecture — frontend

The frontend (libs/ui) mirrors the same three layers, plus a composition layer that wires them together for each screen.

Domain layer ​

  • Entity — plain TypeScript types, e.g. Transaction.
  • Usecase — business logic modeled as a finite state machine: a pure getNextState(state, action) function, agnostic to React and to where its data comes from. See the abstract Usecase base class every usecase implements.
  • Repository interface — the data contract a usecase depends on, e.g. TransactionRepository, with real, mock, and URL-query-backed implementations satisfying it interchangeably.

Data layer ​

Implements the repository interfaces using the OpenAPI-generated client, translating API payloads into domain entities. A parallel mock implementation satisfies the exact same interface, which is what lets every usecase be unit-tested without a network call.

Presentation layer ​

The frontend presentation layer has four parts, each with one job:

  1. Controller — a thin hook (useController) that plugs a usecase's state machine into React via useReducer, e.g. useTransactionListController.
  2. Handler — combines one or more controllers for a screen, maps domain entities to UI props, and wires cross-usecase effects (e.g. re-fetching the list after a delete succeeds). Example: TransactionListHandler.
  3. Screen — pure Tamagui JSX driven entirely by props, with no business logic of its own. Example: TransactionListScreen.
  4. App composition — the outermost piece, in libs/ui/src/app, instantiates the real API repositories and usecases and renders the Handler. app/ and presentation/screens/ each split into a pos/ and an order/ folder, one per app (libs/ui/src/app/pos, libs/ui/src/app/order), so a screen or composition root belongs to exactly one app. This POS example is the exact component both apps/pos-web and apps/pos-mobile import via @gatherloop-pos/ui/pos — see Cross-Platform (Web + Mobile) for how the same composition serves both.

Both apps/pos-web and apps/order-web are Next.js apps (Pages Router) composed identically: a page file is nothing but a getServerSideProps that resolves request-scoped data (session, route params) server-side and a default export pointing at a composition root — no JSX, no local components, no client-only gates. libs/ui/src/app/order holds four composition roots and an index.ts, the same shape as app/pos: no provider, no context, no hook, no wrapper. See docs/trd-order-app-composition-and-ssr.md for how the order app converged on this from an earlier, client-only design.

Why bother ​

Every layer boundary above is also a testing boundary: usecases are tested against mock repositories with no server or database (Testing Strategy), and either platform's UI can be swapped without rewriting a single business rule — which is exactly how one libs/ui codebase ends up powering both a Next.js web app and a React Native mobile app.

Built with VitePress. Content lives in docs-site/ — see the README for how to run it locally.