ddd-hexagonal-architecture

DDD / Hexagonal Architecture (youteacher_web)

youteacher_web is the Next.js 15 / React 19 front end of YouTeacher, and it is organised as a hexagonal (ports-and-adapters) codebase with a domain-driven core. The point of the arrangement is simple: business rules never learn where the data comes from, and the data plumbing never learns the business rules. Everything else follows from keeping those two apart.

Four layers, one direction of dependence

The src/ tree splits into four layers, and dependencies only ever point inward:

  • domain/ — pure business logic with no framework imports. It holds the Account aggregate and its value objects (email, password, TOTP code, OAuth provider link) plus the jobs entities. Domain objects are immutable: a change returns a new instance rather than mutating in place (for example, linking a provider yields a fresh Account).
  • application/ — use cases and ports. A use case such as LoginUseCase orchestrates domain logic; a port is an interface the use case depends on, never a concrete class. The ports are deliberately narrow — LoginGateway is just login / logout / fetchProfile, StorageGateway is just readUser / persistSession / clearSession — so a use case declares exactly the capabilities it needs and nothing more.
  • infrastructure/ — the adapters that implement those ports against the real world: HTTP clients to the backend services, the auth API gateways, browser storage, recaptcha, navigation. AuthApiGateway implements AuthGateway by delegating to the transport functions; the use case above it never sees fetch.
  • features/ — self-contained UI modules (auth, job-search) with their own components, hooks and utilities.

Dependency injection lives in a React hook

The interesting move is where the concrete adapters get bound to the abstract use cases. There is no DI container; the composition root is a client hook, useAuthService. It instantiates the concrete gateways once (memoised), then constructs each use case by handing it its ports — new LoginUseCase(authGateway, recaptcha, storage) — and returns the ready-wired use cases to the components. The feature layer is therefore the only place that knows both the interface and its implementation; the domain and application layers stay ignorant of both React and the network.

This is what makes the code testable without mocking the world: a unit test constructs the same use case with fake ports (login: vi.fn(), a stub storage) and asserts the orchestration — that recaptcha ran, that the gateway was called with the captcha token, that the session was persisted. Swapping the real HTTP adapter for a test double changes nothing in the use case.

The auth bounded contexts

The auth domain is documented as five bounded contexts that all compose through the single Account aggregate: Identity (the account itself), Access (session adoption/revocation), Credentials (password rules), MFA (TOTP / WebAuthn state), and Social Linking (OAuth provider links). Transport DTOs are mapped into these domain types at the boundary, so the wire format of any one backend service never leaks inward. One design detail worth naming at the architecture level: the session itself is held server-side in an httpOnly cookie, and the client persists only non-secret user info locally.

Related

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →