Skip to content

ADR-0014: Domain-Driven Frontend Structure

Accepted Cross-Project

Date: 2026-03-08

Context

The frontend needs to organize Vue components, stores, routes, types, and tests for 15+ business domains (projects, issues, sprints, epics, lanes, teams, users, time logs, etc.). Some domains are nested: issues belong to projects, sprints belong to projects, comments belong to issues.

Two common approaches exist:

  1. Technical layerscomponents/, stores/, services/, types/ at the top level, with all domains mixed within each layer
  2. Vertical slices — each domain is a self-contained directory with its own components, store, types, routes, and mocks

The Problem for AI Agents

Without documentation, AI agents default to the technical-layer approach because it's more common in tutorials and starter templates. When asked to "add a new feature for labels," an AI would create components/LabelList.vue, stores/labels.ts, types/label.d.ts — scattering the feature across the codebase instead of creating a cohesive domains/labels/ directory.

Decision

The frontend uses vertical slices organized by business domain. Each domain is a self-contained directory under frontend/src/domains/.

Top-Level Domain Structure

domains/[name]/
├── store.ts           # Adapter-store factory (~20 lines)
├── constants.ts       # DOMAIN_NAME for REST URLs
├── types.d.ts         # TypeScript types (Resource, Adapted, New)
├── route.ts           # Vue Router config for this domain
├── components/        # Domain-specific Vue components
├── relations/         # Nested sub-domains (optional)
└── __mocks__/         # Vitest mocks
    └── store.ts       # Mock store matching real store API

Nested Relation Structure

Domains that belong to a parent domain live under relations/:

domains/projects/
├── store.ts
├── constants.ts
├── types.d.ts
├── route.ts
├── components/
├── __mocks__/
└── relations/
    ├── issues/        # Same structure as top-level domain
    │   ├── store.ts
    │   ├── constants.ts
    │   ├── types.d.ts
    │   ├── components/
    │   └── __mocks__/
    ├── sprints/
    ├── epics/
    ├── lanes/
    ├── attachments/
    └── systemPrompts/

Current Domain Map

Top-Level DomainRelations
auth/
github/
projects/issues/, sprints/, epics/, lanes/, attachments/, systemPrompts/
teams/
timeLogs/
users/

Store Pattern: Top-Level vs Relation

Top-level stores are singletons created at module scope:

ts
// domains/teams/store.ts
const teamAdapter = (resource: TeamResource): Team => ({
    ...resourceAdapter(resource),
    projects: () => relationshipAdapterConstructor<ProjectResource, Project>(resource.projectIds, projectStore),
    members: () => relationshipAdapterConstructor<UserResource, User>(resource.memberIds, userStore),
});

export const teamStore = adapterStoreModuleFactory<TeamResource, Team, NewTeam>(TEAM_DOMAIN_NAME, teamAdapter);

Relation stores are factory functions that create per-parent instances:

ts
// domains/projects/relations/issues/store.ts
const generateRestURL = (projectId: number): string =>
    `${PROJECT_DOMAIN_NAME}/${projectId}/${ISSUE_RELATION_NAME}`;

export const makeIssueStoreForProject = (projectId: number) => {
    const issueAdapter = makeIssueAdapterForProject(projectId);
    return adapterStoreModuleFactory<IssueResource, Issue, NewIssue>(
        generateRestURL(projectId), issueAdapter
    );
};

Constants Pattern

Each domain defines its REST URL segment as a constant:

ts
// domains/projects/constants.ts
export const PROJECT_DOMAIN_NAME = 'projects';

// domains/projects/relations/issues/constants.ts
export const ISSUE_RELATION_NAME = 'issues';

REST URLs are composed from these constants: projects/${projectId}/issues.

Mock Pattern

Every domain has __mocks__/store.ts that mirrors the real store API:

ts
// domains/projects/__mocks__/store.ts
export const projectStore = {
    getAll: computed<Partial<Project>[]>(() => projectsRef.value),
    getById: vi.fn<() => ComputedRef<Partial<ProjectResource> | undefined>>(() => computed(() => undefined)),
    getOrFailById: vi.fn<() => Promise<Partial<Project>>>(),
    generateNew: mockGenerateNew,
    retrieveAll: vi.fn<() => Promise<void>>(),
};

Tests import from the mock path, never from the real store.

Architecture Test Enforcement

frontend/tests/js/architecture/domain-structure.spec.ts enforces:

  • Every top-level domain must have types.d.ts and __mocks__/
  • Every relation must have constants.ts, types.d.ts, and __mocks__/
  • Every store.ts must use adapterStoreModuleFactory (with explicit exceptions list)

Options Considered

OptionVerdictReason
Technical layers (components/, stores/, services/)RejectedWith 15+ domains, each layer becomes a flat list of unrelated files. Adding a feature touches 4+ directories. AI agents struggle to understand which files belong together.
Vertical slices by business domainAcceptedSelf-contained domains. Adding is additive (create one directory), removing is subtractive (delete one directory). Architecture tests enforce the structure.

Consequences

Positive

  • New domains follow a predictable pattern — AI agents can generate the scaffold from any existing domain
  • Import paths are intuitive: domains/projects/relations/issues/store reads like the business relationship
  • Tests and mocks live next to the code they test — no cross-directory hunting
  • Deleting a domain is one rm -rf — no orphaned files in other directories
  • Architecture tests catch structural violations at CI time

Negative

  • Deep nesting: domains/projects/relations/issues/components/ShowIssueModal.vue is 5 levels deep
  • Cross-domain imports require reaching up to ../../ or using path aliases
  • New developers must learn the convention before contributing (no standard Vue tutorial covers this)
  • AI agents without access to this ADR will suggest flat-file structures

References

  • Architecture test: frontend/tests/js/architecture/domain-structure.spec.ts
  • Import boundary test: frontend/tests/js/architecture/import-boundaries.spec.ts
  • Adapter-Store Pattern — explains the store factory used in each domain

Architecture documentation for contributors and collaborators.