A clean monorepo is not about putting everything in one repository. It is about establishing clear, testable boundaries between domain, application, and infrastructure while giving teams a single source of truth for shared types, utilities, and CI. In practice, Clean and Hexagonal patterns help you keep the core logic independent from frameworks, databases, and delivery mechanisms. The result is code that scales with your team and survives technology churn.

This guide shows how to structure a TypeScript monorepo around explicit boundaries, define ports and adapters, enforce contracts with types, and use dependency injection to select infrastructure at composition time. We will cover folder conventions, module APIs, request and response schemas, testing strategy, CI and release hygiene, and the anti-patterns that quietly erode maintainability.

Architectural baseline: the domain layer holds your business entities, value objects, invariants, and domain services. It has zero imports from frameworks. The application layer orchestrates use cases, transactions, and policies. It operates on domain types and exposes input and output ports for the outside world. The infrastructure layer provides concrete adapters for databases, HTTP, queues, storage, and third-party services. Interfaces point inward, implementations point outward. This inverted dependency keeps the core pure and testable.

Why a monorepo for this style: when there are many domain modules and micro frontends, separate repos cause duplicated contracts, drift in versions, and fragile CI. A monorepo ensures one version of truth for shared types and packages, fast refactors across boundaries, and atomic releases when you change contracts. Tooling such as pnpm workspaces or Turborepo allows incremental builds and caching so developers do not pay for unrelated packages.

Package layout that scales: use small, purpose-driven packages. Example groups include domain modules (auth, blog, product, payments), application services (use-cases per bounded context), interface adapters (controllers, presenters, serializers), infrastructure adapters (database repositories, HTTP clients, file storage), and platform shells (web app, admin app, worker, cli). Each package has its own tsconfig, tests, and a clear public API defined by index.ts. Avoid barrel files that re-export everything by default; export only what you promise to support.

Contracts first: define request and response contracts at the application boundary as TS types and runtime-validated schemas. For example, pair TypeScript interfaces with a validation library so that external inputs are checked at runtime and internal code can rely on static guarantees. Keep DTOs separate from domain entities so your core models do not leak transport concerns. When you must evolve a contract, add versioned DTOs or compatibility mappers rather than breaking changes.

Dependency injection glues the system: the application and domain layers depend on interfaces, not on concrete classes. At runtime, a composition root in each platform shell wires implementations for those interfaces. For example, the web server may inject a Postgres repository, while a test shell injects a fast in-memory repository. This keeps tests deterministic and makes swapping infrastructure a configuration change instead of a rewrite.

Use case orchestration lives in the application layer. A use case receives validated input, loads aggregates via repository ports, performs domain operations, persists changes, and returns output models or presenters. Keep use cases thin but explicit. They define policies and permissions, enforce transaction boundaries, and emit domain events for cross-cutting reactions such as indexing, notifications, or audit logging.

Ports and adapters in practice: define small interfaces that do one job well, such as IUsersRepository with methods that match aggregates and invariants, not raw SQL tables. Adapters implement those ports using whatever technology you choose. Keep mapping between persistence models and domain entities in the adapter, not the entity. This prevents your domain from knowing about persistence-specific concerns like ObjectId or Date serialization.

Bounded contexts reduce coupling. Group domain and application code by business capability, not by technical layer only. Inside each context, you will still have domain, application, and adapter subfolders, but the top-level grouping reflects the business language. This makes it easier to reason about ownership, testing focus, and release cadence.

Testing pyramid with fast feedback: unit-test domain entities and services with no I/O. These tests should run in milliseconds and cover invariants and edge cases. For application use cases, write integration tests against in-memory or lightweight adapters to validate orchestration and policies. For infrastructure, focus on a handful of contract tests per adapter to confirm that implementations satisfy the port behavior against real services. End-to-end tests verify critical flows and are kept few and stable.

Transactions and consistency: let the application layer control transactional boundaries via a unit-of-work or transactional port. Adapters implement the details for the database or message bus. Expose a transaction-aware context to use cases when needed, but avoid leaking it into domain entities. If you need eventual consistency, model domain events as first-class outputs of use cases and handle them in separate subscribers with their own retry and idempotency policies.

Error handling strategy: use a small hierarchy of application errors that map to transport-level responses in controllers. Domain errors represent violated invariants and are part of the use case contract. Convert unexpected infrastructure failures into typed application errors with safe messages and structured metadata for observability. Always log correlation IDs and minimal context so you can trace a failed request across services without leaking sensitive data.

Observability at the boundary: instrument use cases, ports, and adapters. Capture timings for repository calls, outbound HTTP, and queue operations. Emit structured logs with request IDs and user IDs where allowed. For privacy, keep PII out of logs and redact by default. Add health checks and readiness probes for each platform shell so orchestration can roll safely.

Versioning and releases: treat each package as semantically versioned, even inside the monorepo. When you change an interface in a shared package, bump its version and update consumers atomically in one PR. Use automated release notes and a changelog discipline for public packages. For internal-only packages, lightweight version bumps are fine, but keep the dependency graph clean and avoid circular imports.

CI and caching: cache install, build, and test outputs per package and per environment. Run affected tests only for changed packages and their dependents. Enforce formatting, lint rules, and API surface checks in CI. A broken main branch is expensive; require fast pre-merge checks and keep the pipeline under 10 minutes for common paths. Nightly full runs can be longer.

Security posture: centralize secret management via environment-injected providers and never read secrets directly in domain or application code. Add allowlists for outbound domains in HTTP adapters. Audit dependencies and pin versions in shared configs. Threat-model privileged adapters such as admin controllers or migration tools and apply stricter authentication and authorization policies.

Migration without pain: when replacing a database or changing a message broker, write a second adapter that satisfies the same ports and can run side by side. Introduce a feature flag in the composition root to switch traffic per environment. Run shadow writes and read-only verification before full cutover. This technique also enables blue-green deploys for risky changes in serialization or indexing.

Documentation that matters: create a living architecture map in the repo with diagrams for layers, ports, and data flows. For each package, include a short README that explains public API, responsibilities, and non-goals. Document the composition root and environment variables for each platform shell. Keep examples minimal and runnable.

Governance without bureaucracy: encode rules in code. Lint against forbidden imports from infrastructure into domain. Enforce public API surfaces with type tests. Add a precommit hook that fails if packages import from private folders. Small, automated constraints are more reliable than manual reviews.

Team workflow: assign ownership per bounded context and publish a service contract for cross-context calls. Use consumer-driven tests for shared APIs. Keep PRs small and focused on one boundary at a time. Reserve design docs for cross-cutting changes like adding a new event bus or refactoring authentication across contexts.

Case study sketch: a marketplace monorepo with user, catalog, and orders contexts. The user context defines domain entities for Account and Session, ports for AccountsRepository and TokenService, and use cases for RegisterUser and Login. The orders context depends only on the user ports for identity verification. When the team replaced JWT with a different token strategy, only the adapter and composition changed. Domain and use cases were untouched, and tests remained valid.

Common failure modes: letting frameworks leak into domain types, sharing mutable utility singletons across packages, allowing repositories to expose raw ORM models, mixing DTOs and domain entities, skipping runtime validation at the boundary, writing God services that know everything, and adding cross-package imports that bypass public APIs. Each of these shortcuts makes testing harder and refactoring dangerous.

The payoff: clean boundaries reduce cognitive load, make onboarding faster, and allow teams to move independently. When you can swap an adapter without touching domain code, run use cases in isolation, and evolve contracts with confidence, your monorepo becomes an accelerator instead of a drag.

Interfaces point inward, implementations point outward. Protect the core and everything else can evolve without fear.

From scattered code to stable boundaries

  • Organize by bounded context, then by layer inside each context.
  • Define ports as small, focused interfaces and keep adapters thin.
  • Validate inputs at the edge and keep DTOs separate from domain entities.
  • Use a composition root per platform shell to wire dependencies.
  • Write fast unit tests for domain, integration tests for use cases, and a few stable end-to-end tests.
  • Control transactions via an application-level unit-of-work port.
  • Instrument ports and adapters with structured logs and metrics.
  • Version shared packages and update consumers atomically.
  • Enforce architectural rules with linting and import constraints.
  • Introduce new infrastructure behind adapters and switch via flags.