# Vue Ddd Architecture

> Build and maintain scalable Vue 3 + TypeScript applications using a layered, module-per-domain architecture (domain / application / infrastructure / ui) with enforced dependency boundaries and a public API per module. Use this skill whenever the user adds a feature or a business module to a Vue app, bootstraps a new Vue project, asks where a file or piece of logic belongs, wonders whether their layering is correct, or wants to refactor a messy pages/components Vue codebase into modules — even if they never say the words "architecture" or "DDD". Triggers EN: "add a module", "new feature in Vue", "structure my Vue app", "DDD frontend", "where should this service live", "split this component", "refactor to modules", "scalable Vue architecture", "domain layer", "clean architecture Vue". Триггеры RU: «добавь модуль», «новая фича», «разложи по слоям», «куда положить этот файл», «структура Vue-проекта», «DDD на фронтенде», «отрефактори на модули», «масштабируемая архитектура», «доменный слой», «сделай по-нормальному

- Skill: `magersoft/vue-ddd-architecture` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add magersoft/vue-ddd-architecture`
- Raw SKILL.md: https://api.skillmd.com/api/skills/magersoft/vue-ddd-architecture/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: magersoft (https://skillmd.com/u/magersoft)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/magersoft/vue-ddd-architecture

---


# Vue DDD Architecture

A layered, module-per-domain architecture for Vue 3 + TypeScript applications, distilled
from a production codebase. It gives a growing app a shape that stays navigable: business
logic lives in modules, each module has four layers with one direction of dependency, and
each module exposes exactly one public API.

## Be honest about what this is

This is **DDD-inspired layering, not tactical DDD**. There are no aggregates, no value
objects, no domain events, no rich entities with behaviour. On the frontend, the domain
model is usually already owned by the backend, and reimplementing it in the browser buys
complexity without buying correctness.

What this architecture actually delivers is narrower and more useful: business logic is
separated from transport and from presentation, every file has one obvious home, modules
can be deleted or extracted without archaeology, and the boundaries are machine-checkable
by a linter rather than by memory.

Say this plainly if the user calls it "DDD" and expects aggregates. Do not invent a
domain model that the app does not need.

## Scope

Vue 3 with the Composition API and TypeScript. The layering ideas port to React and other
frameworks, but this skill deliberately does not cover them — the concrete guidance below
(composables as the unit of composition, Pinia stores, `unref`) is Vue-specific, and vague
framework-neutral advice would produce worse code than no advice.

## The shape

```
src/
├── app/          composition root: router, layouts, providers, config, global styles
├── modules/      business modules — the app's actual subject matter
└── shared/       reusable, business-free building blocks (portable to another project)
```

A module:

```
modules/<module>/
├── domain/           types + pure factories. No side effects, no imports of runtime code.
├── application/      use cases. services/ + the use<Module>() facade = the public API.
├── infrastructure/   models/ (talks to the network) + stores/ (holds state)
├── ui/               views/ and components/, each component owning its composables/ and styles/
├── utils/            mappers and helpers specific to this module's subject matter
├── routes.ts         this module's route declarations (entity/screen modules)
└── index.ts          the public API — the only thing other modules may import
```

Read `references/layers.md` for what belongs in each layer, with examples of the mistakes
that keep recurring.

## The one rule that makes this worth doing

**Dependencies point one way, and modules talk to each other only through their front door.**

```
app ──▶ modules ──▶ shared
```

- `shared` knows nothing about `app` or `modules`. It should survive being copy-pasted into
  an unrelated project.
- `modules` never import from `app`.
- A module imports another module only as `@/modules/<other>` — never
  `@/modules/other/infrastructure/stores`. Deep imports are what turn a modular app back
  into a ball of mud, one "just this once" at a time.

Inside a module:

```
ui ──▶ application ──▶ infrastructure ──▶ domain
```

`ui` never imports `infrastructure` directly. When a component needs data, it needs a use
case, and the use case is what should be edited when the requirement changes.

These rules are enforced by ESLint, not by good intentions — see
`references/eslint-boundaries.md`. Set that up early; a boundary that only exists in prose
degrades within weeks.

## Deciding where code goes

Ask, in order:

1. **Does it talk to the outside world or hold state?** → `infrastructure`
   (HTTP endpoints in `models/`, reactive state in `stores/`).
2. **Does it describe something the application *does*?** → `application/services/`.
   "Fetch the incomes and put them in the store, tracking loading and errors" is a use
   case, even though it feels plumbing-ish.
3. **Is it a type, or a factory for an initial value?** → `domain/`.
4. **Is it a pure transformation of this module's data?** → `utils/`
   (mappers, groupers, formatters tied to the subject matter).
5. **Is it rendering, or logic that exists only to serve one component?** → `ui/`.
6. **Is it none of the above and has no business meaning at all?** → `shared/`.

The test that resolves most arguments: *if the backend changed shape, which files would
you edit?* Those are infrastructure. *If the product requirement changed, which files
would you edit?* Those are application.

## What counts as a module

Not everything deserves a module, and not every module needs every layer. Creating empty
layers "for symmetry" is the most common way this architecture accumulates noise.

| Kind | Recognise it by | Required layers |
|---|---|---|
| **Entity module** | owns an API resource (`incomes`, `wallets`, `categories`) | all four |
| **Screen module** | a page composing other modules' data (`dashboard`, `welcome`) | `application` + `ui`; no models, usually no store |
| **Domain-service module** | pure calculation over other modules' data (`calculations`, `pricing`) | `domain` + `application` only |
| **Cross-cutting state** | needed by nearly everything, not a subject in itself (`settings`, `theme`, `session`) | see below |

The last row is the one that causes arguments, so decide it with a single question: **who
consumes this?**

- Only `app` — route guards, layouts, the HTTP transport (a session token, the active
  locale) → put it in `app/`. Modules never import `app`, so nothing else can reach it,
  which is correct: nothing else should.
- Business modules consume it too (user settings, selected currency, feature flags) → keep
  it a module with a deliberately wide facade, and consume it as `@/modules/settings`.

What is never right is a module that everything deep-imports. If you find
`@/modules/settings/infrastructure/stores` in twenty files, the fix is not to ban the
import — it is that the facade was never written.

## Workflows

Pick the one that matches the request and read that file before starting.

| Situation | Read |
|---|---|
| Add a new module, or a feature inside an existing one | `references/new-module.md` |
| Set up a new Vue project with this architecture | `references/new-project.md` |
| Restructure an existing pages/components codebase | `references/migration.md` |
| Check that code follows the rules (do this before reporting done) | `references/audit-checklist.md` |
| Wire up the boundary linter | `references/eslint-boundaries.md` |
| Understand a layer in depth | `references/layers.md` |

A complete, real reference module lives in `examples/incomes/` — every layer, CRUD with
pagination, a list component, a form. When writing new code, read it rather than
reconstructing the pattern from the prose above; the prose omits the small things
(barrel files, `unref` placement, error plumbing) that are exactly where generated code
tends to go wrong.

## Adapt to the project, don't overwrite it

Before generating anything, read `package.json` and look at two or three existing modules
or components.

- **State**: Pinia is the default this architecture was built against, but the layer
  contract is what matters — a store exposes state plus setters, and only
  `application/services` mutate it. If the project uses something else, keep the contract
  and change the implementation.
- **HTTP**: `shared/infrastructure/api` owns the transport, behind a four-method
  `ApiClient` interface. The base services depend on that interface, not on any HTTP
  library, so a project already using axios, ky, a generated SDK or `@vueuse/core` keeps
  its client and writes one small adapter — see `assets/adapters/`. Reaching for
  hand-written services instead is almost always the wrong trade: it re-implements loading,
  error and pagination behaviour per module, which is exactly the drift the primitives
  exist to prevent.
- **UI kit, styling, i18n**: not this skill's business. Match whatever the project already
  does — Vant or shadcn-vue, CSS modules or Tailwind, i18n or plain strings. The only UI
  rule that is architectural is that components reach data through `application`, and that
  non-trivial component logic lives in a composable next to the component instead of
  swelling the `<script setup>` block.
- **OpenAPI**: if the backend publishes a schema, generating DTO types is the cheapest
  possible domain layer. If it doesn't, hand-write the types — the structure is unchanged.

## When the project doesn't fit, say so out loud

Sometimes the project genuinely resists part of this architecture — an HTTP client that
does not map onto `ApiClient`, a state library without the store contract, a module
boundary the existing code contradicts. Deviating is often the right answer; deviating
*silently* never is, because the person reading the diff cannot tell a deliberate
adaptation from a misunderstanding.

Stop and put the choice to them: name what does not fit, the two or three ways forward, and
which you would pick. A brief question costs a minute. Discovering the divergence later, in
a summary or during review, costs the trust that makes the rest of the guidance worth
following.

## Scaffolding

`scripts/scaffold-module.mjs` creates the skeleton — every directory, every barrel file,
the `index.ts`, and minimal valid contents:

```bash
node scripts/scaffold-module.mjs --name incomes --kind entity --root src
```

`--kind` is one of `entity`, `screen`, `domain-service` and controls which layers appear.
Run it first, then fill in the real content. It exists because the file that gets forgotten
is always `index.ts`, and a module without its public API silently invites deep imports
from everywhere else.

## Before you say it's done

Run the project's own checks — typecheck, lint, build — and read
`references/audit-checklist.md`. The boundary rules are easy to break accidentally while
concentrating on behaviour, and the linter catches most of it, but not the parts about
side effects in `domain/` or a service that has quietly grown a second responsibility.

