Zum Inhalt springen

Signals machen schlechte Architektur nicht besser

Signals sind gut.

Wirklich.

Feingranulare Reaktivität, klareres Dependency-Tracking, weniger indirekte Subscription-Mechanik, bessere lokale Ableitbarkeit von Zustand — das sind echte Verbesserungen.

Aber Signals sind ein Reaktivitätsprimitive.

Keine Architektur.

Und genau dort beginnt das Missverständnis.

Ein neues Reaktivitätsmodell löst keine falschen Verantwortlichkeiten. Es verhindert keine DTO-Leakage. Es zieht keine Schichtgrenzen. Es entscheidet nicht, wem Zustand gehört. Es sorgt nicht automatisch dafür, dass Komponenten klein bleiben.

Wer vorher schlechte Struktur hatte, kann mit Signals sehr schnell reagierenden schlechten Code bauen.

Bad vs Good

Neue Framework-Features erzeugen oft eine kurze Phase kollektiver Hoffnung.

Endlich wird alles einfacher.

Endlich weniger Boilerplate.

Endlich keine Subscription-Hölle mehr.

Endlich kein RxJS für jeden kleinen UI-Zustand.

Und ja: vieles davon stimmt.

Signals können Code deutlich vereinfachen. Sie können lokale Reaktivität verständlicher machen. Sie können Ableitungen näher an den Zustand bringen. Sie können verhindern, dass man für triviale UI-Zustände ein halbes Observable-Theater aufführt.

Aber sie beantworten keine Architekturfragen.

Sie beantworten nicht:

  • Wo lebt fachlicher Zustand?
  • Wer darf ihn ändern?
  • Wo endet der UI-Layer?
  • Wo werden DTOs übersetzt?
  • Wo werden Use-Cases orchestriert?
  • Wie reagieren mehrere Teile der Anwendung auf ein fachliches Ereignis?
  • Welche Schicht kennt Infrastruktur?
  • Welche Schicht kennt Darstellung?

Signals geben dir einen sehr guten Draht.

Sie sagen dir nicht, wo du ihn verlegen sollst.

Wenn eine API-Struktur direkt in ein Signal geschrieben wird, ist sie immer noch im falschen Layer.

type TaskDto = {
task_id: string;
created_at: string;
assigned_to_user_id: string | null;
};
@Component(...)
export class TaskCardComponent {
readonly task = signal<TaskDto | null>(null);
}

Das ist nicht besser, nur weil task jetzt ein Signal ist.

Die Komponente kennt weiterhin API-Feldnamen. Das Template kennt weiterhin Transportdetails. created_at ist weiterhin ein String. assigned_to_user_id: null muss weiterhin irgendwo fachlich interpretiert werden.

Signals machen daraus keine saubere Grenze.

Sie machen nur die falsche Grenze reaktiv.

Besser ist weiterhin:

type TaskViewModel = {
id: string;
createdAtLabel: string;
assigneeLabel: string;
};
function toTaskViewModel(dto: TaskDto): TaskViewModel {
return {
id: dto.task_id,
createdAtLabel: formatDate(dto.created_at),
assigneeLabel: dto.assigned_to_user_id ?? 'Unzugewiesen',
};
}

Die UI sollte ein ViewModel konsumieren.

Nicht das Transportmodell der API.

Ein Signal in einer Komponente ist nicht automatisch schlechte Architektur.

Lokaler UI-State ist dort völlig okay.

@Component(...)
export class OrderDialogComponent {
readonly open = signal(false);
show() {
this.open.set(true);
}
close() {
this.open.set(false);
}
}

Das ist UI-Verhalten.

Problematisch wird es, wenn die Komponente fachliche Abläufe besitzt.

@Component(...)
export class OrderComponent {
readonly loading = signal(false);
readonly error = signal<string | null>(null);
constructor(
private readonly http: HttpClient,
private readonly router: Router,
private readonly toast: ToastService,
) {}
submitOrder(data: OrderData) {
if (data.items.length === 0) {
this.error.set('Bestellung ohne Positionen ist nicht erlaubt');
return;
}
const payload = this.mapToPayload(data);
this.loading.set(true);
this.http.post('/api/orders', payload).subscribe({
next: () => {
this.toast.success('Bestellung gespeichert');
this.router.navigate(['/confirmation']);
},
error: () => {
this.error.set('Speichern fehlgeschlagen');
},
complete: () => {
this.loading.set(false);
},
});
}
}

Das Problem ist nicht signal().

Das Problem ist, dass die Komponente plötzlich alles besitzt:

  • fachliche Regel
  • Mapping
  • HTTP
  • Loading-State
  • Error-State
  • Toast
  • Navigation
  • Ablaufsteuerung

Das war vorher mit Observables schlecht.

Mit Signals ist es immer noch schlecht.

Nur etwas moderner geschrieben.

Reaktive schlechte Architektur

Signals können überall auftauchen:

In Komponenten. In Services. In Stores. In Facades. In Mappers. In irgendwelchen Utility-Klassen, in denen sie besser nicht wären.

Das macht sie flexibel.

Und genau deshalb gefährlich.

Wenn ein Team kein klares Ownership-Modell für Zustand hat, entstehen schnell Fragen wie:

  • Welches Signal ist die Quelle der Wahrheit?
  • Wer darf .set() aufrufen?
  • Ist das Signal lokal oder fachlich?
  • Ist es UI-State oder Application-State?
  • Warum wird derselbe Zustand an drei Stellen gespiegelt?
  • Warum hat ein Service ein Signal, eine Komponente ein Signal und der Store auch?

Dann hat man keine Architektur.

Man hat verteilte Reaktivität.

@Injectable()
export class OrderService {
readonly selectedOrderId = signal<string | null>(null);
}
@Component(...)
export class OrderListComponent {
readonly selectedOrderId = signal<string | null>(null);
}
@Injectable()
export class OrderFacade {
readonly selectedOrderId = signal<string | null>(null);
}

Das ist nicht automatisch falsch.

Aber ohne klare Regel ist es verdächtig.

Denn jetzt existiert dieselbe fachliche Idee an mehreren Orten.

Und irgendwann fragt jemand:

Warum ist die Detailansicht auf Order 42, aber die Liste markiert Order 17?

Die Antwort lautet meistens nicht: „Signals sind schlecht.“

Die Antwort lautet:

Niemand hat entschieden, wem dieser Zustand gehört.

Signals ohne Ownership sind globale Variablen mit Extra-Schritten

Abschnitt betitelt „Signals ohne Ownership sind globale Variablen mit Extra-Schritten“

Ein Signal ist verführerisch einfach.

export const currentUser = signal<User | null>(null);

Fertig.

Überall importieren. Überall lesen. Überall schreiben.

Was soll schon passieren?

Eine Menge.

Denn damit ist currentUser zwar reaktiv, aber nicht architektonisch kontrolliert.

Wer darf den User setzen? Wann wird er zurückgesetzt? Was passiert beim Logout? Was passiert bei Token Refresh? Was passiert bei Mandantenwechsel? Welche Tests müssen das vorbereiten? Welche Komponente hängt indirekt daran?

Ein globales Signal ohne Ownership ist keine State-Management-Lösung.

Es ist globaler Zustand.

Mit hübscherem API.

Signals sind dann stark, wenn sie innerhalb klarer Grenzen eingesetzt werden.

Zum Beispiel:

Component
konsumiert ViewModel
sendet Intent
Facade / Store
besitzt Zustand
koordiniert UI-nahe Flüsse
bietet abgeleitete Signals an
Use Case / Application Service
führt fachliche Abläufe aus
Infrastructure
spricht mit API, Storage, externen Systemen

Dann passen Signals sehr gut.

@Injectable()
export class OrderFacade {
private readonly store = inject(OrderStore);
readonly vm = this.store.vm;
readonly isLoading = this.store.isLoading;
loadOrders() {
this.store.loadOrders();
}
submitOrder(data: OrderFormValue) {
this.store.submitOrder(data);
}
}

Die Komponente bleibt langweilig:

@Component(...)
export class OrderComponent {
private readonly facade = inject(OrderFacade);
readonly vm = this.facade.vm;
submit(data: OrderFormValue) {
this.facade.submitOrder(data);
}
}

Die Komponente weiß nicht, was danach passiert.

Sie weiß nicht, ob ein HTTP-Call ausgeführt wird. Sie weiß nicht, ob ein Toast erscheint. Sie weiß nicht, ob navigiert wird. Sie weiß nicht, ob eine Liste neu geladen wird.

Sie sendet einen Intent.

Das ist ihre Aufgabe.

Signals mit Ownership

Ein Signal Store kann eine sehr gute Application Facade sein.

Aber auch ein Store kann schlecht geschnitten sein.

Wenn alles in einem Store landet, entsteht nur eine neue Datenhalde:

type AppState = {
user: User | null;
orders: Order[];
selectedOrderId: string | null;
dialogOpen: boolean;
formDraft: OrderFormValue | null;
permissions: string[];
theme: string;
toastMessage: string | null;
tempFlag: boolean;
};

Das ist nicht besser, nur weil es Signal Store ist.

Ein Store braucht Grenzen.

Besser:

AuthStore
user
permissions
OrdersStore
orders
selectedOrderId
loading
error
commands
OrderDialogStore
local dialog state
draft state

Nicht jeder Zustand gehört in denselben Ort.

Signals machen Zustand leicht.

Gerade deshalb muss man entscheiden, welcher Zustand wohin gehört.

computed() ist großartig für Ableitungen.

readonly fullName = computed(() => {
const user = this.user();
if (!user) {
return 'Unbekannt';
}
return `${user.firstName} ${user.lastName}`;
});

Das ist harmlos.

Aber auch hier gibt es eine Grenze.

Wenn in computed() plötzlich komplexe fachliche Regeln landen, ist Vorsicht angebracht.

readonly discount = computed(() => {
const order = this.order();
if (!order) {
return 0;
}
if (order.customer.type === 'premium' && order.items.length > 10) {
return 0.15;
}
if (order.customer.country === 'DE' && order.total > 1000) {
return 0.1;
}
return 0;
});

Vielleicht ist das okay.

Vielleicht ist das aber eine fachliche Regel, die nicht in einer UI-nahen Ableitung wohnen sollte.

Die Frage lautet nicht:

Darf ich computed verwenden?

Natürlich.

Die Frage lautet:

Ist das eine Darstellungableitung oder eine fachliche Entscheidung?

Darstellungableitungen passen gut in Store, Facade oder ViewModel-Mapping.

Fachliche Entscheidungen gehören in einen Use Case, eine Domain-Funktion oder eine Application-Schicht.

Auch effect() ist kein Architekturkonzept.

Ein Effect kann sinnvoll sein, um auf Zustandsänderungen zu reagieren.

Aber wenn Effects wahllos über Komponenten und Services verteilt werden, entsteht wieder unsichtbare Kopplung.

effect(() => {
if (this.saved()) {
this.toast.success('Gespeichert');
this.router.navigate(['/orders']);
this.reload();
}
});

Das kann funktionieren.

Aber man sollte sehr genau wissen, warum dieser Effect an dieser Stelle sitzt.

Denn Effects sind Reaktionen.

Reaktionen brauchen Ownership.

Wer besitzt den Save-Flow? Wer entscheidet über Navigation? Wer entscheidet über Toasts? Wer lädt welche Ressource neu? Wie testet man das? Was passiert, wenn eine zweite Stelle ebenfalls auf saved() reagiert?

Ohne Konzept wird aus effect() schnell:

Irgendwo passiert dann halt noch etwas.

Das ist keine Architektur.

Das ist reaktiver Nebel.

Eine gute Signals-Architektur ist nicht spektakulär.

Sie sieht ungefähr so aus:

DTO rein
Mapper übersetzt
Store hält Zustand
computed erzeugt ViewModel
Component rendert
Intent geht raus
Use Case läuft
Result aktualisiert Zustand

Das ist nicht neu.

Das ist der Punkt.

Signals ändern nicht die Grundprinzipien guter Frontend-Architektur.

Sie machen nur manche Implementierungen angenehmer.

Der Unterschied liegt nicht darin, ob man signal(), Observable, Subject, Redux oder einen Signal Store verwendet.

Der Unterschied liegt darin, ob Zuständigkeiten klar sind.

Signals sind gut geeignet für:

  • lokalen UI-State
  • abgeleiteten Zustand
  • ViewModels
  • Feature Stores
  • Facades
  • lesbare reaktive Datenflüsse
  • feingranulare Updates

Signals lösen aber nicht automatisch:

  • Layertrennung
  • DTO-Mapping
  • Use-Case-Orchestrierung
  • Ownership von Zustand
  • saubere Event-Flüsse
  • fachliche Modellierung
  • Testbarkeit
  • Komponentenverantwortung

Die technische Frage lautet:

Kann ich das mit Signals bauen?

Die architektonische Frage lautet:

Sollte dieser Zustand oder diese Entscheidung hier wohnen?

Die zweite Frage ist wichtiger.

Signals sind ein gutes Werkzeug.

Aber sie sind kein Ersatz für Architektur.

Sie verbessern die Implementierung guter Strukturen. Sie retten keine schlechten Strukturen.

Wer DTOs direkt in Komponenten schiebt, hat mit Signals immer noch DTO-Leakage.

Wer Use-Cases in Komponenten schreibt, hat mit Signals immer noch Business-Logik im UI-Layer.

Wer Zustand ohne Ownership verteilt, hat mit Signals immer noch unkontrollierten Zustand.

Wer Seiteneffekte wahllos in Effects versteckt, hat mit Signals immer noch schwer nachvollziehbare Flüsse.

Signals machen reaktive Programmierung in Angular angenehmer.

Aber sie beantworten nicht die alte, unbequeme Frage:

Wer ist wofür verantwortlich?

Die kurze Regel:

Signals machen guten Code besser lesbar. Schlechte Architektur machen sie nur schneller sichtbar.

Oder etwas gemeiner:

Wer vorher Spaghetti gebaut hat, bekommt mit Signals keine Lasagne. Nur feinere Nudeln.