„Soll ich das nach shared verschieben?“ ist wahrscheinlich die häufigste Architekturfrage in einem Angular-Projekt. Die kurze Antwort: seltener, als du denkst. shared ist für generischen (domänenfreien) und stabilen Code. Alles, was du dort ablegst, wird zur Abhängigkeit mehrerer Features — und jede Änderung daran kann sie alle kaputtmachen.
Verschiebe nur das nach shared, was bereits in mehreren Features verwendet wird, keine Domänenbegriffe enthält und keine Datenabhängigkeiten hat. Andernfalls bleibt die Komponente neben dem Code, der sie verwendet.
Warum: Eine einzelne Verwendung rechtfertigt keine Abstraktion. Du weißt noch nicht, welche Variationen auftauchen werden.
Nächster Schritt: Lass sie dort. Bewerte neu, sobald die zweite (oder dritte) echte Verwendung auftaucht.
Checkliste vor dem Verschieben
Bevor du etwas nach shared verschiebst, beantworte alle Punkte mit JA:
[ ] Es wird JETZT von mindestens 2 verschiedenen Features verwendet (nicht „könnte sein“)
[ ] Name und API enthalten keine Domänenbegriffe
[ ] Es injiziert keine Stores, Daten-Services oder den Router
[ ] Es kommuniziert nur über input / output / model / ng-content
[ ] Alle Verwendungen haben dasselbe Verhalten (keine Flags pro Verwendung)
[ ] Du bist bereit, die API wie eine öffentliche zu behandeln (jede Änderung betrifft alle)Richtig extrahieren: Generisches nach shared, Domäne ins Feature
Du verschiebst eine Komponente selten „so wie sie ist“. Meist teilst du sie auf: Der generische Teil (Aussehen, Verhalten) geht nach shared/ui, und der Domänenteil (Labels, Statusfarben, Regeln) bleibt als dünner Wrapper im Feature.
// shared/ui/badge.ts — generisch, ohne Domäne
@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 — Domänen-Wrapper, bleibt im 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 — verwendet in orders UND im 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'));
}Domänenkomponenten, die mehrere Features nutzen
UserAvatar wird in Orders, Comments und Admin verwendet. Er ist nicht generisch (es geht um Benutzer), aber er wird geteilt. Die Lösung: Er bleibt im besitzenden Feature (users) und wird über eine öffentliche API bereitgestellt. So ist klar, wer ihn pflegt, und der Rest des Users-Features bleibt privat.
// features/users/index.ts — das „Schaufenster“ des Users-Features für den Rest der Anwendung
export { UserAvatar } from './ui/user-avatar';
export type { User } from './models/user';
// alles andere (der interne Store, Seiten, Mapper) bleibt privat
// features/orders/ui/order-row.ts
import { UserAvatar } from '@features/users'; // ✓ über die öffentliche API
// import { UserAvatar } from '@features/users/ui/user-avatar'; ✗ innerhalb eines anderen FeaturesAnzeichen, dass eine Shared-Komponente zurück ins Feature muss
// Das Zeichen, dass eine Shared-Komponente aufgeteilt (oder zurück ins Feature verschoben) werden muss
@Component({ selector: 'app-card' })
export class Card {
readonly compact = input(false);
readonly showPrice = input(true);
readonly isCheckout = input(false); // ← ein Feature „sickert“ in shared durch
readonly hideAvatarOnMobile = input(false);
readonly legacyMode = input(false); // ← niemand weiß mehr, warum es existiert
}- Inputs nach dem Muster „Ich werde auf Seite X verwendet“.
- Jede Änderung, die ein Feature verlangt, erfordert die Prüfung aller anderen.
- Nur noch ein Feature verwendet die Komponente.
Wo welcher Ordner liegt, zeigt Ordnerstruktur; die Regeln, wer was importieren darf, Barrels, Aliase & Grenzen.
Wann ja, wann nein
- Buttons, Cards, Badges, Dialoge, generische Tabellen, Paginatoren.
- UI-Direktiven und Pipes: Tooltip, Autofocus, timeAgo.
- Layouts: Page Header, Empty State, Skeleton.
- Alles mit Domänennamen:
ProductCard,InvoiceTable,UserFilters. - Komponenten, die in einem einzigen Feature verwendet werden (auch an 10 Stellen).
- Komponenten, die Stores oder APIs injizieren.
Zwei fast identische Komponenten in zwei Features, die sich unabhängig entwickeln, sind leichter zu warten als eine gemeinsame Komponente voller ifs. Warte, bis du klar siehst, was wirklich gemeinsam ist (die „Rule of Three“).