“Should I move it to shared?” is probably the most frequent architecture question in an Angular project. The short answer: less often than you think. shared is for generic (domain-free) and stable code. Anything you put there becomes a dependency of several features — and any change to it can break them all.
Move to shared only what is already used in several features, without domain terms and without data dependencies. Otherwise, the component stays next to the code that uses it.
Why: A single use does not justify an abstraction. You don’t know yet which variations will appear.
Next step: Leave it there. Reassess when the second (or third) real use appears.
Checklist before moving
Before moving something to shared, answer YES to all of these:
[ ] It is used NOW by at least 2 different features (not “it might be”)
[ ] Its name and API contain no domain terms
[ ] It does not inject stores, data services or the Router
[ ] It communicates only through input / output / model / ng-content
[ ] All the uses have the same behaviour (no per-use flags)
[ ] You are willing to treat its API as a public one (any change affects everyone)How to extract correctly: generic into shared, domain into the feature
You rarely move a component “as is”. Usually you split it: the generic part (look, behaviour) goes into shared/ui, and the domain part (labels, status colours, rules) stays in the feature as a thin wrapper.
// shared/ui/badge.ts — generic, no domain
@Component({
selector: 'app-badge',
host: { '[class]': '"badge " + tone()' },
template: `<ng-content />`,
})
export class Badge {
readonly tone = input<'neutral' | 'success' | 'danger' | 'warning'>('neutral');
}
// features/orders/ui/order-status-badge.ts — domain wrapper, stays in the feature
@Component({
selector: 'app-order-status-badge',
imports: [Badge],
template: `<app-badge [tone]="tone()">{{ label() }}</app-badge>`,
})
export class OrderStatusBadge {
readonly status = input.required<OrderStatus>();
protected readonly label = computed(() => ORDER_LABELS[this.status()]);
protected readonly tone = computed(() => (this.status() === 'cancelled' ? 'danger' : 'success'));
}// features/orders/ui/order-status-badge.ts — used in orders AND in the dashboard
@Component({
selector: 'app-order-status-badge',
template: `<span [class]="'badge ' + color()">{{ label() }}</span>`,
})
export class OrderStatusBadge {
readonly status = input.required<OrderStatus>();
protected readonly label = computed(() => ORDER_LABELS[this.status()]);
protected readonly color = computed(() => (this.status() === 'cancelled' ? 'red' : 'green'));
}Domain components used by several features
UserAvatar is used in orders, comments and admin. It is not generic (it is about users), but it is shared. The solution: it stays in the owning feature (users) and is exposed through a public API. That way it is clear who maintains it, and the rest of the users feature stays private.
// features/users/index.ts — the “showcase” of the users feature for the rest of the application
export { UserAvatar } from './ui/user-avatar';
export type { User } from './models/user';
// everything else (the internal store, pages, mappers) stays private
// features/orders/ui/order-row.ts
import { UserAvatar } from '@features/users'; // ✓ through the public API
// import { UserAvatar } from '@features/users/ui/user-avatar'; ✗ inside another featureSigns that a shared component must be pulled back into the feature
// The sign that a shared component must be split (or moved back into the feature)
@Component({ selector: 'app-card' })
export class Card {
readonly compact = input(false);
readonly showPrice = input(true);
readonly isCheckout = input(false); // ← a feature “leaks” into shared
readonly hideAvatarOnMobile = input(false);
readonly legacyMode = input(false); // ← nobody knows why it exists any more
}- Inputs of the kind “I am used on page X”.
- Every change requested by one feature requires checking all the others.
- Only one feature still uses the component.
Where each folder lives is covered in Folder structure; the rules on who may import what, in Barrels, aliases & boundaries.
When YES, when NO
- Buttons, cards, badges, dialogs, generic tables, paginators.
- UI directives and pipes: tooltip, autofocus, timeAgo.
- Layouts: page header, empty state, skeleton.
- Anything with a domain name:
ProductCard,InvoiceTable,UserFilters. - Components used in one feature (even if in 10 places).
- Components that inject stores or APIs.
Two almost identical components in two features that evolve independently are easier to maintain than one common component full of ifs. Wait until you see clearly what is common (the “rule of three”).