
Architecting Angular Applications - Teil 4: Services
Veröffentlicht am 12. Juli 2025 - ⏱️ Ca. (wird berechnet) min. Lesezeit
Komponenten sollen schlank sein und bilden den grundlegenden UI Baustein. Mit Direktiven können wir nun wiederverwendbare Strukturen im HTML bauen. Eine Art Decorator für HTML. Damit ist das Fundament gelegt und wir widmen uns Angulars Büchse der Pandora: Den Services. Die Analogie trifft, denn man weiss in einem Angular Projekt oftmals nicht, was einen in einem Service erwartet
Angular gibt Services kaum Grenzen vor. Alles, was `@Injectable()` ist, darf überall rein. Das verleiht sehr viel Freiheit und Macht. Ein Segen für die Produktivität, ein Risiko für die Wartbarkeit.
In diesem Teil der Serie werden wir Ordnung in das Service Chaos bringen. Ich zeige, was Services eigentlich sind und wie sie sich Anhand der Methodik "ASU-D" sinnvoll unterscheiden lassen. Gegen Ende des Artikels kommt ein Ausblick warum Services kein Sammelbecken sein müssen. Warum es sinnvoll ist, den entscheidenden Schritt weiterzugehen.
Was ist eigentlich ein Angular-Service?
Technisch ist die Antwort einfach: Eine Klasse, die mit `@Injectable()` markiert ist und über Angulars Dependency Injection an jede beliebige Stelle injiziert werden kann. Sie ist standardmäßig ein Singleton und wird automatisch vom Framework verwaltet.
Doch genau diese Einfachheit macht Services gefährlich universell. Ein Service kann API-Zugriffe machen, globale Zustände halten, Berechnungen durchführen oder technische Utilities bereitstellen. Alles ist möglich. Alles darf überall rein.
Das Resultat: Services wirken wie die natürliche Heimat für alles, was nicht direkt in eine Component passt. Wer keinen Plan hat, schreibt erstmal einen Service.
An dieser Stelle lohnt sich ein Blick über den Tellerrand. Wie macht es die Konkurrenz von Angular? In React etwa arbeitet man mit Hooks. Im Gegensatz zu Angular möchte man hier funktionaler, expliziter und modularer arbeiten. Spannend wird es, wenn wir auch noch Vue betrachten, denn Vue geht in dieselbe Richtung: Dort gibt es keine Services im Angular-Sinn. Stattdessen setzt man voll auf sogenannte „Composables“. Das sind kompakte Logik-Bausteine, die klar abgegrenzt und wiederverwendbar sind. Vue hat zwar selber keine formalen Schichten und bei wachsender App Grösse seine eigenen Probleme, dennoch ist es eine interessante Beobachtung, dass sowohl React als auch Vue hier einen anderen Weg gehen.
Wie sieht das dort in der Praxis aus: Ein Login-Vorgang? Kommt in `useLogin.ts`. Das Nutzerprofil? `useUserProfile.ts`. Avatar ändern? `useAvatar.ts`. Jedes Anliegen wird in ein eigenes Composable ausgelagert. Vue fördert Zerlegung und propagiert - anders als Angular - eine dezentrale Architektur. Keine Singleton-Sammelstellen, sondern modulare Datenflüsse und klarer Ownership. Es ist kein Zufall, dass das Vue-Ökosystem auf zentrale Stores wie Pinia setzt und diese als dedizierte Composables behandelt. Zusammengefasst haben sich also sowohl Vue als auch React entschlossen, bei all der Flexibilität, die sie sonst predigen, den Entwickler bei der Businesslogik stärker in Richtung Modularität zu leiten.
Angular verzichtet bewusst darauf, bei den Services zu starke Vorgaben zu machen. Services geben dem Entwickler Gestaltungsfreiheit zurück, bei einem ansonsten recht streng geführten Framework. Aber das ist der Knackpunkt. Viele Entwickler haben sich daran gewöhnt von Angular an der Hand genommen zu werden. Die plötzliche Freiheit bei den Services überfordert. Sie werden so schnell zu einem Container, einer Not- oder Sammellösung. Und das ist der Kern des Problems: Container neigen dazu, sich mit allem zu füllen, was man nicht anders unterbringt.
Das grosse Sammelbecken
Wer sich in der Praxis durch Code arbeitet, stößt schnell auf verdächtige Gebilde: `UserService`, `AppService`, `DataService`. Namen, die alles und nichts bedeuten und in denen oft genau das steckt: alles und nichts.
Der typische `UserService` beginnt mit einem Login-Aufruf. Bald kommen ein paar Session-Methoden dazu. Dann vielleicht lokale Speicherung, Rollenprüfung, Avatar-Update, Mailversand, Passwort-Reset. Irgendwann hängt sogar das Admin-Panel an ihm.
Diese Services wachsen schleichend. Mit jeder neuen Funktion schleppen sie neue Abhängigkeiten und Seiteneffekte mit sich. Unit-Tests? Die bleiben als Erste liegen. Die Motivation von einst muss dem Pragmatismus weichen, denn es gilt Deadlines zu halten. Gerade das wird auch immer schwerer, denn den Überblick haben meist nur noch die alten Hasen im Projekt. Wer neu dazu kommt, der fragt oder zieht den Kopf ein.
Das Muster dahinter: Wenn allein das Wort „Service“ bleibt und keine inhaltliche Beschreibung mehr folgt, ist Vorsicht geboten. Es fehlt nicht an Funktionen, sondern an Strukturen. An Typisierung, an Layern oder schlicht: An Architektur. Drum merke: Ein Sammelbecken ist kein Architekturprinzip.
ASU-D
Wir konnten bereits bei React und Vue eine interessante Beobachtung machen: Dort wird die Businesslogik stärker modularisiert bzw. zerteilt. Das ist eine starke Beobachtung, die sich auf Angular übertragen lässt. Nicht jeder Service ist gleich. Um das Sammelbecken zu strukturieren, bietet es sich an, zunächst Services zu kategorisieren.
Daher nutze ich den Moment und möchte hier eine neue Methodik vorschlagen. Im nächsten Artikel werde ich genauer auf die Hintergründe eingehen, aus der sich diese Methodik zusammensetzt. Konkret führe ich hier Erkenntnisse aus Domain Driven Design und Clean Architecture zusammen. Auf Angular übertragen möchte ich mein Konzept als "ASU-D" vorstellen. Benannt nach den vier Bestandteilen:
Angular Services können in vier Arten gegliedert werden:
- Adapter-Services kommunizieren mit externen APIs. Sie senden HTTP-Requests, verarbeiten Responses, aber kennen keine interne Logik. Sie folgen dem Adapter Pattern.
- Store-Services halten UI-Zustände wie Filter, Pagination oder aktuelle Selektion.
Sie sind meist lokal zum UI gedacht und reagieren auf Nutzerinteraktionen. In diesem Fall unbedingt
das
providers
Feature nutzen. Aber auch globaler State kann so gut abgebildet werden. - Utility-Services bieten technische Helfer: Parser, Formatter, UUID-Generatoren. Sie haben keine Seiteneffekte, speichern nichts und greifen auf nichts zu. Sie sind reine Dienstleister.
- Domain-Services enthalten Regeln oder komplexere Operationen aus der Fachlichkeit. Hier lebt die eigentliche Intelligenz der Applikation, die konsumiert, orchestriert und verwaltet.
Diese Einteilung ist nicht absolut, aber sie schärft das Bewusstsein. Wer von vornherein benennt, was ein Service sein soll, verhindert, dass er alles wird.
Das ist eine hilfreiche mentale Stütze, aber soll keinesfalls mental bleiben! In der Praxis sehe ich oft einen inflationären Ordner mit dem Namen: services.
Folgender Anwendungsfall: Ein Entwickler ist relativ neu in einem Projekt und muss nun von einer API Daten beziehen und sie danach formatieren. Woher weis dieser Entwickler, dass es so einen Code in der Applikation nicht schon gibt? Er macht den riesigen Services Ordner auf, scannt ihn kurz und denkt puh. Vlt. noch einen Kollegen fragen. Okay dem fällt auch nichts ein. Gerade in grossen Applikationen ist das kein seltenes Phänomen!
Ich spreche da aus meiner eigenen leidvollen Erfahrung. Genau das sorgt für duplizierten Code. Einige Zeit später kommt ein dritter Entwickler und will wieder so etwas Ähnliches machen. Er findet jetzt aber vielleicht gleich 2 Stellen, die eigentlich das tun, was er machen möchte. Welche ist richtig? Ist eine veraltet? Warum gibt es das zweimal? Fragen, auf die er keine Antwort erhalten wird, Pandoras Box wurde geöffnet.
Genau hier lässt sich ansetzen. Angenommen der Entwickler findet nun ein Projekt vor, welches die Services von Anfang an strukturiert hat. Hierzu wurde auf einen globalen Services-Ordner verzichtet, stattdessen gibt es gleich vier Verzeichnisse: adapters, stores, utilities, domains. Eine ganz klare Verbesserung!
Der Entwickler weis: Er will Daten via API beziehen. Wenn es das schon gibt, dann muss es ein Adapter sein. Ausserdem möchte er die Daten formatieren. Also eine Utility. Natürlich lebt auch dieses Konzept von der Disziplin der Entwickler. Aber es bietet eine hervorragende Guidance und lässt sich einfach in Angulars bestehendes Modell integrieren. Wie ich bereits erwähnte: Architektur ist kein einmaliges Vergnügen. Es ist eine Disziplin und braucht Disziplin. Jeden Tag aufs Neue.
Wie sehen dann gute Services aus?
Ein Service ist nur so gut wie seine Verantwortung. Und die sollte glasklar sein. Die wichtigste Regel lautet: Eine Klasse, eine Aufgabe. Das klingt simpel, ist aber in der Praxis alles andere als selbstverständlich. Vor allem, wenn man in Eile ist oder sich die App gerade schnell verändert.
Die zuvor eingeführten vier Kategorien helfen nicht nur bei der Struktur, sondern auch beim Schreiben
guter Services. Wer sie kennt, kann bewusster entscheiden, was wohin gehört. Um das zu verdeutlichen,
nehmen wir ein Praxisbeispiel. Wir möchten einen Checkout schreiben. Der Service stellt eine
order()
Funktion bereit, welche von einer Component angestossen wird. Statt eines
Monolithen nutzen wir aber die hier vorgeschlagenen Kategorien
Adapter Services
Der OrderApiAdapter
übernimmt klar abgegrenzt die API-Kommunikation. Er weiss nichts über
den internen Zustand der Anwendung und trifft keine Entscheidungen. Seine Aufgabe ist es, einen Auftrag
auszulösen, nicht mehr und nicht weniger. In seiner simpelsten Form sieht er daher so aus:
@Injectable({ providedIn: 'root' })
export class OrderApiAdapter {
constructor(private http: HttpClient) {}
order(payload: OrderDto): Observable<OrderResponse> {
return this.http.post<OrderResponse>('/api/orders', payload);
}
}
Store Services
Unsere Checkout-Komponente braucht Informationen über den aktuellen Warenkorb, gewählte Zahlungsmethoden oder Lieferadressen. In unserem einfachen Beispiel lässt sich das gut global abbilden. Weitere gute Store Beispiele wären der Warenkorb oder der aktuelle Benutzer, der gerade einkauft.
@Injectable({ providedIn: 'root' })
export class CheckoutStore {
private readonly paymentMethod$ = new BehaviorSubject<PaymentMethod | null>(null);
private readonly deliveryAddress$ = new BehaviorSubject<Address | null>(null);
setPaymentMethod(method: PaymentMethod): void {
this.paymentMethod$.next(method);
}
getPaymentMethod(): Observable<PaymentMethod | null> {
return this.paymentMethod$.asObservable();
}
}
Ein Store-Service speichert temporäre UI-Zustände, ist aber selbst UI-frei. Die Komponente konsumiert ihn, um darauf zu reagieren oder Eingaben zu speichern. Wichtig: Kein HTTP, kein Mapping, keine Navigation. Stores sind reine Daten Behälter
Utility Services
Nun zum unscheinbarsten Teil: Formatierungen und Hilfslogik. Der Gesamtpreis soll inklusive
Mehrwertsteuer angezeigt werden, Beträge brauchen ein Währungsformat. Das ist die Domäne von Utilities.
Als Beispiel nehmen wir den PriceFormatterUtility
:
Hinweis: Der ein oder andere fragt sich vielleicht, ob dieses Beispiel nicht besser in einer Pipe versorgt wäre. Ja und nein. Pipes sind primär zur Verwendung in Templates gedacht. Elegant verknüpfen lässt sich dieses Beispiel, indem die Pipe diesen Utility Service injected und die Logik wiederverwendet.
@Injectable({ providedIn: 'root' })
export class PriceFormatterUtility {
format(amount: number): string {
return amount.toFixed(2).replace('.', ',') + ' CHF';
}
}
Der PriceFormatterUtility
kennt keine Steuersätze, keine Produkte und keinen Warenkorb. Er
rechnet
und formatiert. Punkt. Das macht ihn testbar, robust und universell einsetzbar. Und hier wird bereits
sichtbar: Das sind ganz klassische Funktionen, die gerne in der Applikation mehrfach erfunden und gebaut
werden. Weil sie nicht sichtbar und klar abgelegt wurden.
Domain Services
Hier kommt alles zusammen. Der Domain service erfüllt einen klaren Use Case. In unserem Beispiel den Checkout:
@Injectable({ providedIn: 'root' })
export class CheckoutService {
constructor(
private cartStore: CartStore,
private checkoutStore: CheckoutStore,
private priceFormatter: PriceFormatterUtility,
private orderApi: OrderApiAdapter
) {}
submitOrder(): Observable<string> {
const products = this.cartStore.getProducts();
const payment = this.checkoutStore.getPaymentMethod();
const address = this.checkoutStore.getDeliveryAddress();
if (!payment || !address) {
throw new Error('Checkout information incomplete');
}
const total = this.calculateTotal(products);
const payload: OrderDto = {
products,
paymentMethod: payment,
deliveryAddress: address,
totalFormatted: this.priceFormatter.format(total)
};
return this.orderApi.order(payload).pipe(map(res => res.orderId));
}
private calculateTotal(products: Product[]): number {
const subtotal = products.reduce((sum, p) => sum + p.price, 0);
const vat = subtotal * 0.077;
return subtotal + vat;
}
}
Hier lebt die eigentliche Intelligenz. Sie kennt die Regeln, orchestriert Werte und entscheidet. Aber sie speichert nichts und ruft keine externen Systeme auf. So bleibt sie klar, stabil und leicht testbar. Gleichzeit ist das aber der einzige Layer der alle anderen sehen darf. In der Domäne kommen Adapter, Utilities und Stores zusammen. Die Domäne ist das Bindeglied, der Stratege.
Fassen wir zusammen: Ein einzelner Anwendungsfall, vier Rollen, vier Services. Jeder mit einer klaren Aufgabe. Jeder mit einem sprechenden Namen. Wer so arbeitet, reduziert nicht nur Komplexität, sondern erleichtert auch Einarbeitung, Tests und langfristige Wartbarkeit.
Moment: Was heisst hier lokal bzw. global?
Es wurde nur kurz erwähnt, aber es verdient eine eigene Betrachtung. Mit providers
hat
Angular ein Feature, mit dem gesteuert werden kann, wo ein Service überhaupt lebt. Gemeint ist Angulars
Dependency Injection.
Ein Service ist per Default ein Singleton. Ein Singleton könnte aber auch eine statische Klasse sein.
Richtig spannend wird es, wenn wir einen lokalen Store betrachten. Was heisst das? Nun angenommen ein
komplexes Formular soll einen Store haben. In diesem Formular lassen sich Modale öffnen, die dann
auf die gleichen Daten zugreifen sollen. Genau das leistet providers
. Die
Formularkomponente macht via providers
klar: Wenn ich erzeugt werde, dann soll mit mir
eine neue Instanz der in providers genannten Services erzeugt werden. Wenn nun später im Dependency Tree
eine Modal Komponente ganz normal diesen Store injected, dann erhält sie exakt diese lokale Instanz.
Innerhalb dieses Mirco Kosmos kann effizient via Store gearbeitet werden. Stirbt die Komponente, die
die Instanz erzeugt hat, dann wird auch der Store zerstört.
Das ist ein sehr effizientes Werkzeug. Nicht jeder Store muss immer global sein. Gerade wenn es möglich ist, dass eine Komponente mehrmals gleichzeitig in der Anwendung existiert, dann wird ein globaler Store zur Komplexitätsfalle. Hier helfen lokale Stores.
Und was ist eigentlich mit Signals?
Signals verändern die Art, wie Zustände in Angular modelliert werden und damit auch, wie Services strukturiert sind. In diesem Artikel habe ich sie bewusst ausgeklammert, denn ihr Einfluss verdient eine eigene Betrachtung.
Wer sich für die architektonische Einordnung von Signals interessiert, findet dazu demnächst eine vertiefende Analyse in meinem Beitrag auf Heise Developer, welche sich gut als Teil 4.5 einschieben lässt. Sobald er veröffentlicht wurde, wird er hier verlinkt sein. Wer ihn nicht verpassen möchte, kann sich auch gerne für meinen Newsletter anmelden.
Ausblick: Wenn Angular nicht mehr genügt
ASU-D bringt Struktur und ist genau deshalb so wirkungsvoll für kleine und mittelgroße Applikationen. Doch wenn Anwendungen wachsen, wenn Teams größer werden und Use Cases sich über Jahre verändern, genügt Struktur allein nicht mehr. Dann geht es nicht nur um Organisation von Services, sondern um Langlebigkeit und Isolation von Verantwortung.
In großen Systemen entsteht ein neuer Anspruch: Die Businesslogik soll nicht einfach nur gut gegliedert sein. Sie sollte unabhängig funktionieren. Entkoppelt vom UI, vom Framework, von Angular selbst. Die Applikation wird zum Produkt im Produkt. Nur so kann in der schnelllebigen JS Frontend Welt echte Langlebigkeit sichergestellt werden. Im letzten Teil der Serie werde ich daher Angular den Rücken kehren. Die Businesslogik wandert in eine eigene losgelöst Schicht.
Fazit: ASU-D schließt die Büchse der Pandora
Services sind Angulars Geschenk für Gestaltungsfreiheit und Flexibilität. Aber wie bei jedem Geschenk liegt es am Empfänger, wie verantwortungsvoll er damit umgeht. Angular öffnet die Büchse der Pandora weit. Wer sie schliessen will, muss selbst Struktur schaffen.
Mit ASU-D liegt ein pragmatischer Ansatz auf dem Tisch: Statt willkürlicher Sammelstellen entstehen klar benannte Dienste mit fokussierten Rollen. Adapter für APIs, Stores für UI-Zustände, Utilities für technische Helfer, Domains für echte Fachlogik. Diese Einteilung ist kein Dogma, sondern eine Entscheidungshilfe. Leicht zu verstehen, aber tief in der Architektur verankert.
Wer ASU-D lebt, schafft Übersicht. Wer Übersicht schafft, verhindert Wildwuchs. Und wer Wildwuchs verhindert, gewinnt langfristig Wartbarkeit und Klarheit. Services werden nicht weniger, aber besser.