{"id":295,"date":"2026-08-29T18:47:06","date_gmt":"2026-08-29T18:47:06","guid":{"rendered":"https:\/\/www.fabricioruch.ch\/?p=295"},"modified":"2026-08-29T18:47:06","modified_gmt":"2026-08-29T18:47:06","slug":"der-god-service-ist-kein-service","status":"publish","type":"post","link":"https:\/\/www.fabricioruch.ch\/?p=295","title":{"rendered":"Der God Service ist kein Service"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Die Function ist d\u00fcnn. Der Reviewer nickt. Dann \u00f6ffnet man die Klasse, an die sie delegiert.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class SignupUser\n{\n    private readonly SignupService _signupService;\n\n    &#91;Function(\"SignupUser\")]\n    public async Task&lt;IActionResult&gt; Run(...)\n    {\n        var payload = await frc.GetPayLoad&lt;SignupRequest&gt;();\n        await _signupService.CreateSignup(payload.Signup, eventId, frc, payload.UserId);\n        return new StatusCodeResult(201);\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Das sieht nach Schicht aus. Der Einstieg kennt weder Tabelle noch Graph. Genau das habe ich an anderer Stelle gelobt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der Fehler sitzt eine Datei tiefer. Nicht im Controller. Im Ding, das <em>Service<\/em> heisst.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Der Konstruktor ist die Diagnose<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>public class SignupService : ISignupService\n{\n    private readonly GraphApiService _graphApiService;\n    private readonly EmailService _emailService;\n    private readonly ICalendarService _calendarService;\n    private readonly StorageService _storageService;\n    private readonly ExcelService _excelService;\n    private readonly AdminService _adminService;\n    private readonly PermissionsService _permissionsService;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Sieben Abh\u00e4ngigkeiten. Davon mehrere konkret. Persistenz, Verzeichnis, Kalender, Mail, Export, Rechte, Vorlagen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das ist kein Service. Das ist ein Betriebssystem mit Methodennamen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein Service hat einen Grund, sich zu \u00e4ndern. Diese Klasse hat sieben. \u00c4ndert sich Graph, muss sie mit. \u00c4ndert sich das Mail-Template, muss sie mit. \u00c4ndert sich das Kalender-SDK, muss sie mit. \u00c4ndert sich die Table-Entity, muss sie mit. \u00c4ndert sich die Excel-Library, muss sie mit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Hohe Kopplung, niedrige Koh\u00e4sion. Der Name <code>SignupService<\/code> beschreibt nicht, was die Klasse <em>ist<\/em>. Er beschreibt nur, \u00fcber welchen Use Case sie irgendwann gewachsen ist.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Eine Methode, f\u00fcnf Jobs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>CreateSignup<\/code> ist der eigentliche Beweis. Nicht die Zeilenzahl. Die Jobs, die nacheinander in derselben Methode sterben.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zuerst Verzeichnis:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>User user = await _graphApiService.GetUserById(userId, frc);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dann Persistenz. Mehrfach. Header, Slots, bestehende Anmeldungen, Einladungen, Warteliste \u2014 alles \u00fcber <code>GetTableClient&lt;T&gt;()<\/code> in derselben Methode.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dann die Fachregel, die hier tats\u00e4chlich hingeh\u00f6rt: Ist der Slot voll? Kommt die Person auf die Warteliste? Darf sie sich anmelden?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dann Kalender. Eintr\u00e4ge anlegen, alte l\u00f6schen, Attachments aus Blob ziehen, f\u00fcr G\u00e4ste ICS bauen, Graph-Attachments zur\u00fcck in die Tabelle schreiben.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dann Mail. Ablehnung, Warteliste, Best\u00e4tigung intern, Best\u00e4tigung extern, Umbuchung, Benachrichtigung an die Organisatoren. Jeder Pfad holt Templates, filtert Sprachen, trennt Gast und Mitarbeiter, ruft dieselbe fette <code>SendMailToRecipientType<\/code>-Methode.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Am Ende noch: Warteliste nachr\u00fccken.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das ist kein Use Case. Das ist eine Pipeline aus f\u00fcnf Systemen, die zuf\u00e4llig hinter einem HTTP-Verb steht.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Function durfte d\u00fcnn bleiben, weil die God Class die Dicke \u00fcbernommen hat. <em>Thin controller, fat everything else.<\/em> Das ist keine Schicht. Das ist Verschieben.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Koh\u00e4sion ist ein \u00c4nderungsgrund, kein Gef\u00fchl<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Man merkt niedrige Koh\u00e4sion nicht am Bauch. Man merkt sie an der Frage: <em>Warum muss diese Datei heute auf?<\/em><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><th>\u00c4nderung<\/th><th>Muss der Signup-Service mit?<\/th><th>M\u00fcsste er mit, wenn er ein Service w\u00e4re?<\/th><\/tr><tr><td>Graph-SDK Major<\/td><td>Ja<\/td><td>Nein<\/td><\/tr><tr><td>SMTP statt Graph-Mail<\/td><td>Ja<\/td><td>Nein<\/td><\/tr><tr><td>Neues Kalender-Produkt<\/td><td>Ja<\/td><td>Nein<\/td><\/tr><tr><td>Table-Entity um ein Feld<\/td><td>Ja<\/td><td>Nein<\/td><\/tr><tr><td>Excel-Export-Format<\/td><td>Ja<\/td><td>Nein<\/td><\/tr><tr><td>Anmelde-Regel: Slot voll<\/td><td>Ja<\/td><td>Ja<\/td><\/tr><tr><td>Wartelisten-Logik<\/td><td>Ja<\/td><td>Ja<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Zwei Ja in der rechten Spalte. F\u00fcnf in der linken. Die Klasse verdient den Namen f\u00fcr die zwei. Die anderen f\u00fcnf hat sie geschluckt, weil sie <em>in der N\u00e4he<\/em> lagen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das ist der Entstehungsmechanismus. Nicht B\u00f6swilligkeit. Bequemlichkeit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der Kalender geh\u00f6rt \u201eirgendwie zur Anmeldung\u201c. Mail auch. Rechte auch. Also kommt <code>_calendarService<\/code> in den Konstruktor. Dann <code>_emailService<\/code>. Dann noch schnell der Blob f\u00fcr die Anh\u00e4nge. Nach zwei Jahren ist die Anmelde-Regel nicht mehr findbar. Sie sitzt zwischen <code>GetUserById<\/code> und <code>SendMailToRecipientType<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Der Nachbar ist derselbe Fehler mit anderem Etikett<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Neben dem Signup-Service steht oft eine <code>EmailService<\/code> ohne Interface. Der Name klingt fokussiert. Der Konstruktor nicht:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class EmailService\n{\n    private readonly StorageService _storageService;\n    private readonly IMailService _mailService;\n    private readonly GraphApiService _graphService;\n    private readonly QueueService _queueService;\n    private readonly JobLogService _jobLogService;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SendMailToRecipientType<\/code> liest Header, Einladungen, Anmeldungen, letzte Mails. Baut Empf\u00e4ngerlisten. Schreibt Payloads in Blob. Enqueued Jobs. Schreibt Job-Logs. Und irgendwo ganz unten ruft sie den schmalen Transport <code>IMailService.SendMailAsync<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der Transport ist die einzige echte F\u00e4higkeit. Alles davor ist Orchestrierung, Persistenz und Verzeichnis \u2014 in einer Klasse, die <em>Email<\/em> heisst, weil der letzte Schritt eine Mail ist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Caller h\u00e4ngen an der konkreten Klasse. Es gibt kein <code>IMailOrchestrator<\/code>. Es gibt acht Stellen, die <code>SendMailToRecipientType<\/code> kennen, inklusive der Parameter f\u00fcr ICS, Sprachen und <code>isManual<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zwei God Services, die sich gegenseitig rufen. Das ist keine Architektur. Das ist ein Kreisverkehr.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Ein Interface macht ihn nicht schmaler<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SignupService : ISignupService<\/code> existiert. In der DI wird <code>SignupService<\/code> registriert. Die Function injiziert <code>SignupService<\/code>. Das Interface ist die Inhaltsangabe einer zu dicken Klasse.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dasselbe gilt f\u00fcr zwanzig Methoden auf dem Interface. <code>CreateSignup<\/code>, <code>DeclineAll<\/code>, <code>DeleteSignups<\/code>, <code>GetWaitinglist<\/code>, <code>GetAttachments<\/code>, <code>GetSignupsFile<\/code>. Ein Interface mit der Fl\u00e4che eines Backends ist kein Vertrag. Es ist ein Telefonbuch.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein schmaler Service h\u00e4tte ungef\u00e4hr das:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public interface ISignupService\n{\n    Task SignUpAsync(SignupCommand command, CancellationToken cancellationToken);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Kalender, Mail, Warteliste, Export sind dann Ports, die <em>dieser<\/em> Service ruft \u2014 oder, noch besser, Dinge, die <em>nach<\/em> der Anmeldung passieren, nicht <em>in<\/em> ihr.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public sealed class SignupService : ISignupService\n{\n    private readonly ISignupRepository _signups;\n    private readonly IRegistrationPolicy _policy;\n\n    public async Task SignUpAsync(SignupCommand command, CancellationToken cancellationToken)\n    {\n        var decision = await _policy.DecideAsync(command, cancellationToken);\n        await _signups.SaveAsync(decision, cancellationToken);\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Mail und Kalender geh\u00f6ren nicht in dieselbe Transaktion, nur weil sie im selben User-Klick vorkommen. Sie sind Folgen. Folgen kann man ausl\u00f6sen, ohne sie zu <em>sein<\/em>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Warum das Upgrade und der Test dort sterben<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Im vorigen St\u00fcck ging es um den Controller, der Graph kennt. Hier kennt ihn der Service. F\u00fcr das Upgrade ist das derselbe Schaden. Nur besser versteckt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Function bleibt beim Isolated-Worker-Schnitt unber\u00fchrt. Der Reviewer sieht d\u00fcnne Handler. Dann muss <code>CreateSignup<\/code> trotzdem mit, weil sie <code>Microsoft.Graph.Models.User<\/code> h\u00e4lt, Table Clients direkt anspricht, Blob-Container \u00f6ffnet und eine konkrete <code>EmailService<\/code> ruft, die selbst wieder Graph und Queue kennt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein Test f\u00fcr die Anmelde-Regel braucht in diesem Schnitt:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>einen Graph-User<\/li>\n\n\n\n<li>vier Table Clients<\/li>\n\n\n\n<li>Kalender<\/li>\n\n\n\n<li>Mail-Templates<\/li>\n\n\n\n<li>Blob-Streams<\/li>\n\n\n\n<li>Permissions<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Deshalb entstehen Tests, die <code>IStorageService<\/code> mocken und dann nur noch selbst gebaute Testdaten asserten. Die Regel wird nie ausgef\u00fchrt. Die Pipeline ist gr\u00fcn. Die God Class ist ungetestet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das ist keine Testschw\u00e4che. Das ist die Klasse, die sich nicht isolieren l\u00e4sst.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Was man schneidet, ohne das System neu zu bauen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nicht die 800 Zeilen in einem Sprint zerlegen. Die Reihenfolge entscheidet, ob der Schnitt h\u00e4lt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Zuerst den Konstruktor lesen.<\/strong><br>Jede konkrete Abh\u00e4ngigkeit, die nicht die Anmelde-Regel ist, ist ein Kandidat f\u00fcr einen Port. Graph, Mail, Kalender, Excel, Storage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Dann die Methode in Jobs zerlegen, nicht in private Helpers.<\/strong><br><code>private BuildIcsFiles<\/code> in derselben Klasse ist keine Trennung. Es ist Einr\u00fccken. Ein Job wird eine Klasse, wenn er einen eigenen \u00c4nderungsgrund hat.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Die Fachregel zuerst herausziehen.<\/strong><br>Slot voll, Warteliste, Einladung n\u00f6tig. Das ist der einzige Teil, der <code>SignupService<\/code> heissen darf. Er braucht Daten und eine Entscheidung. Kein SDK.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folgen hinter Ports legen.<\/strong><br><code>ICalendarScheduler.Schedule(signup)<\/code>. <code>ISignupNotifier.Notify(signup)<\/code>. Der Signup-Service ruft Absicht, nicht Graph-Attachments.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Persistenz hinter einen Data Service.<\/strong><br>Nicht <code>GetTableClient&lt;SignupEntity&gt;()<\/code> in der Fachmethode. <code>ISignupRepository.Save(signup)<\/code>. Partition Keys sterben eine Schicht tiefer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Mail nicht als God Service stehen lassen.<\/strong><br>Orchestrierung darf existieren. Sie braucht einen Vertrag. Und sie darf nicht selbst Tabelle und Graph sein. Empf\u00e4nger aufl\u00f6sen ist Verzeichnis. Senden ist Transport. Beides ist nicht <code>SendMailToRecipientType<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der Controller bleibt d\u00fcnn. Der Service wird es zum ersten Mal.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Service ist ein Versprechen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Service<\/em> heisst: Hier endet eine Verantwortung. Wer diese Klasse \u00e4ndert, \u00e4ndert eine F\u00e4higkeit. Nicht das halbe System.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein God Service bricht das Versprechen. Er sammelt alles, was in einem Request vorkommt, und nennt die Summe nach dem Request. Signup. Email. Event. Document. Die Function darf dann d\u00fcnn aussehen. Die Abh\u00e4ngigkeit ist nur umgezogen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Man erkennt ihn nicht an der Zeilenzahl. Man erkennt ihn am Konstruktor und an der Frage, warum die Datei heute aufmuss.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn die Antwort mehr als einen Lieferanten und eine Fachregel enth\u00e4lt, ist es kein Service. Es ist die Stelle, an der die n\u00e4chste \u00c4nderung teuer wird \u2014 unabh\u00e4ngig davon, wie sauber der Controller dar\u00fcber sitzt.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Die Function ist d\u00fcnn. Der Reviewer nickt. Dann \u00f6ffnet man die Klasse, an die sie delegiert. Das sieht nach Schicht aus. Der Einstieg kennt weder Tabelle noch Graph. Genau das habe ich an anderer Stelle gelobt. Der Fehler sitzt eine Datei tiefer. Nicht im Controller. Im Ding, das Service heisst. Der Konstruktor ist die Diagnose [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":296,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12,27,26,8,25],"tags":[],"class_list":["post-295","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-computer-science","category-programming-principles","category-quality-assurance-software-engineering","category-software-architecture","category-software-engineering"],"_links":{"self":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/295","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=295"}],"version-history":[{"count":1,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/295\/revisions"}],"predecessor-version":[{"id":297,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/295\/revisions\/297"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/media\/296"}],"wp:attachment":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=295"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=295"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=295"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}