# Cross-Import Resolution Patterns

How to resolve cross-imports between slices on the same layer. Cross-imports
are a code smell, not an absolute prohibition. The strategies below are
ordered, but the right choice depends on the project context.

## What is a cross-import?

A cross-import is an import between different slices within the same layer.
For example:

- importing `features/product` from `features/cart`
- importing `widgets/sidebar` from `widgets/header`

The `shared` and `app` layers do not have slices, so imports within those
layers are not cross-imports.

## Why is this a code smell?

Cross-imports blur domain boundaries and introduce implicit dependencies.
Four concrete problems:

1. **Unclear ownership and responsibility.** When `cart` imports from
   `product`, it becomes unclear which slice owns the shared logic.
   Changes to `product`'s internal implementation can break `cart`
   without warning. This makes bugs harder to localize and code harder
   to reason about.
2. **Reduced isolation and testability.** A core benefit of sliced
   architecture is that each slice can be developed, tested, and deployed
   independently. Cross-imports break this isolation. Testing `cart` now
   requires setting up `product`, and changes in one slice can cause
   unexpected test failures in another.
3. **Increased cognitive load.** Working on `cart` now requires accounting
   for how `product` is structured. As cross-imports accumulate, tracing
   the impact of a change requires following more code across slice
   boundaries.
4. **Path to circular dependencies.** Cross-imports often start as one-way
   dependencies but evolve into bidirectional ones (A imports B, B imports
   A). This locks slices together and makes refactoring increasingly costly.

## Entities layer: prefer boundary merge over @x

Cross-imports in `entities` are usually caused by splitting entities too
granularly. Before reaching for `@x`, consider whether the boundaries should
be merged instead.

The `@x` notation is available as a dedicated cross-import surface for
`entities`, but it should be treated as a **last resort**, a **necessary
compromise**, not a recommended approach. Think of `@x` as an explicit
gateway for unavoidable domain references, not a general-purpose reuse
mechanism. Overuse locks entity boundaries together and makes refactoring
more costly over time.

### How @x works (when boundary merge is genuinely impossible)

Each entity exposes a special `@x/` directory containing files named after
the consuming entity. This makes the cross-import explicit and auditable.

**Direction rule:** in the path `entities/A/@x/B`, **A is the producer and
B is the consumer**. Read it as "A crossed with B": the file `A/@x/B.ts`
is the public API that A exposes specifically for B. So in the example
below, `entities/user/@x/order.ts` is what `user` exposes to `order`, and
`order` imports from it.

```text
entities/
  user/
    @x/
      order.ts          ← Exposed specifically for the order entity
    model/
      user.ts
    index.ts
  order/
    model/
      order-summary.ts  ← Imports from user/@x/order
    index.ts
```

```typescript
// entities/user/@x/order.ts: exposes only what order needs
export { getUserDisplayName } from "../model/user";

// entities/order/model/order-summary.ts
import { getUserDisplayName } from "@/entities/user/@x/order";
```

### Rules when using @x

1. Document why `@x` is needed and why merging boundaries does not apply.
2. Review periodically. Requirements change and `@x` may become unnecessary.
3. Minimize the surface area of `@x` exports.
4. Only between entities. Features and widgets should use Strategy C or D
   below, not `@x`.

## Features and widgets: four strategies

In `features` and `widgets`, multiple strategies are available depending on
project context. Cross-imports here are not always forbidden; they are
dependencies that should be deliberate. The four strategies below are
listed in preferred order, but each fits different situations.

### Strategy A: Slice merge

If two slices are not truly independent and always change together, merge
them into a single larger slice.

```text
// Before: two features that always change together
features/profile/
features/profile-settings/

// After: one cohesive feature
features/profile/
  ui/
    Profile.tsx
    ProfileSettings.tsx
  model/
    profile.ts
    profile-settings.ts
  index.ts
```

If two slices keep cross-importing each other and effectively move as one
unit, they are likely one feature in practice. Merging is often the simpler
and cleaner choice.

### Strategy B: Push shared domain flows down into entities

If multiple features share a domain-level flow, move that flow into a domain
slice inside `entities`. Key principles:

- `entities` contains domain types and domain logic only.
- UI remains in `features` and `widgets`.
- Features import and use the domain logic from `entities`.

For example, if both `features/auth` and `features/profile` need session
validation, place session-related domain functions in `entities/session`
and reuse them from both features.

```text
entities/
  session/
    model/
      validate-session.ts
      session.ts
    index.ts

features/
  auth/
    ui/LoginForm.tsx
    model/login.ts        ← imports validateSession from entities/session
    index.ts
  profile/
    ui/ProfilePanel.tsx
    model/profile.ts      ← imports validateSession from entities/session
    index.ts
```

### Strategy C: Compose from an upper layer (IoC)

Instead of connecting slices within the same layer via cross-imports,
compose them at a higher level (`screens` or `app`). The upper layer assembles
and connects the slices; the slices themselves do not know about each other.

Common Inversion of Control techniques:

- **Render props (React)**: pass components or render functions as props.
- **Slots (Vue)**: use named slots to inject content from parent components.
- **Dependency injection**: pass dependencies through props or context.

#### Basic composition (React)

```typescript
// features/user-profile/index.ts
export { UserProfilePanel } from "./ui/UserProfilePanel";

// features/activity-feed/index.ts
export { ActivityFeed } from "./ui/ActivityFeed";

// screens/UserDashboardPage.tsx
import { UserProfilePanel } from "@/features/user-profile";
import { ActivityFeed } from "@/features/activity-feed";

export const UserDashboardPage = () => (
  <div>
    <UserProfilePanel />
    <ActivityFeed />
  </div>
);
```

`features/user-profile` and `features/activity-feed` do not know about each
other. The screen composes them.

#### Render props (React)

When one feature needs to render content from another, use render props to
invert the dependency:

```typescript
// features/comment-list/ui/CommentList.tsx
interface CommentListProps {
  comments: Comment[];
  renderUserAvatar?: (userId: string) => React.ReactNode;
}

export const CommentList = ({ comments, renderUserAvatar }: CommentListProps) => (
  <ul>
    {comments.map((comment) => (
      <li key={comment.id}>
        {renderUserAvatar?.(comment.userId)}
        <span>{comment.text}</span>
      </li>
    ))}
  </ul>
);

// screens/PostPage.tsx
import { CommentList } from "@/features/comment-list";
import { UserAvatar } from "@/features/user-profile";

export const PostPage = () => (
  <CommentList
    comments={comments}
    renderUserAvatar={(userId) => <UserAvatar userId={userId} />}
  />
);
```

`CommentList` does not import from `user-profile`. The screen injects the
avatar component.

#### Slots (Vue)

Vue's slot system provides a natural way to compose features without
cross-imports:

```vue
<!-- features/comment-list/ui/CommentList.vue -->
<template>
  <ul>
    <li v-for="comment in comments" :key="comment.id">
      <slot name="avatar" :userId="comment.userId" />
      <span>{{ comment.text }}</span>
    </li>
  </ul>
</template>

<!-- screens/PostPage.vue -->
<template>
  <CommentList :comments="comments">
    <template #avatar="{ userId }">
      <UserAvatar :userId="userId" />
    </template>
  </CommentList>
</template>
```

### Strategy D: Cross-feature reuse only via Public API

If strategies A-C do not fit and cross-feature reuse is genuinely
unavoidable, allow it only through an explicit Public API (exported hooks
or UI components). Do not access another slice's `store`, `model`, or
internal implementation.

Unlike strategies A-C which aim to eliminate cross-imports, this strategy
accepts them while minimizing risk through strict boundaries.

```typescript
// features/auth/index.ts
export { useAuth } from "./model/use-auth";
export { AuthButton } from "./ui/AuthButton";

// features/profile/ui/ProfileMenu.tsx
import { useAuth, AuthButton } from "@/features/auth";

export const ProfileMenu = () => {
  const { user } = useAuth();
  if (!user) return <AuthButton />;
  return <div>{user.name}</div>;
};
```

The boundary holds: `features/profile` cannot import from
`@/features/auth/model/internal/*`. Only what `features/auth` explicitly
exposes through `index.ts` is reachable.

The `@x` notation is for the entities layer only. Features and widgets use
strategies A through D above; their access path is the standard public API
(`index.ts`), not a dedicated cross-import surface.

## When to treat a cross-import as a problem

After reviewing these strategies, the question is: when is a cross-import
acceptable to keep, and when should it be treated as a code smell and
refactored?

Common warning signs:

- Directly depending on another slice's `store`, `model`, or business logic
- Deep imports into another slice's internal files (bypassing the public API)
- Bidirectional dependencies (A imports B, and B imports A)
- Changes in one slice frequently breaking another slice
- Flows that should be composed in `screens` or `app`, but are forced into
  cross-imports within the same layer

When these signals appear, treat the cross-import as a code smell and apply
one of the strategies above.

## Strictness depends on project context

The strictness of cross-import enforcement depends on the project:

- In **early-stage products** with heavy experimentation, allowing some
  cross-imports may be a pragmatic speed trade-off.
- In **long-lived or regulated systems** (fintech, large-scale services),
  stricter boundaries pay off in maintainability and stability.

Cross-imports are not an absolute prohibition. They are dependencies that
are generally best avoided, but sometimes used intentionally. If a
cross-import is introduced:

- Treat it as a deliberate architectural choice.
- Document the reasoning in code (a comment explaining why other
  strategies do not apply).
- Revisit it periodically as the system evolves; if requirements change,
  the cross-import may no longer be needed.

## Decision flow for AI agents

```text
Two slices on the same layer need to share code.
  │
  ├─ ENTITIES layer?
  │   ├─ Can boundaries be merged into one entity?
  │   │   └─ YES → Merge. Stop.
  │   └─ Boundaries must stay separate?
  │       └─ Use @x as last resort. Document why merge is not possible.
  │
  └─ FEATURES or WIDGETS layer?
      ├─ Strategy A: Do they always change together?
      │   └─ YES → Merge slices.
      │
      ├─ Strategy B: Is the shared part domain-only logic?
      │   └─ YES → Push down to entities. Keep UI in features.
      │
      ├─ Strategy C: Can the connection be assembled by a higher layer?
      │   └─ YES → Compose in screens or app via render props, slots, or DI.
      │
      └─ Strategy D: Is reuse genuinely unavoidable and the access surface
                     limited to a Public API?
          └─ YES → Allow, but only through index.ts. Never reach into
                   model/, store/, or internal files. Do not use @x in
                   features or widgets.
```

## Anti-patterns

- **Reaching for `@x` in features or widgets.** `@x` is for entities only.
  Use Strategy C (compose) or D (Public API) instead.
- **Treating `@x` as a clean solution.** It is a compromise. If you find
  yourself adding multiple `@x` files between the same entities, the
  boundaries are probably wrong. Merge them.
- **Bypassing the Public API to access internals.** Even when Strategy D is
  in use, importing from `@/features/auth/model/internal/*` defeats the
  purpose. Restrict yourself to what `index.ts` exports.
- **Bidirectional cross-imports.** A imports B and B imports A is almost
  always a sign that the slices should be merged.

## See also

- `references/excessive-entities.md`: prevent the conditions that lead to
  entity-layer cross-imports in the first place.
- `references/layer-structure.md`: layer rules and import directions.
