[{"data":1,"prerenderedAt":97},["ShallowReactive",2],{"content:blog:fuenf-jahre-bestellhistorie-ins-crm":3,"content:blog":34},{"slug":4,"title":5,"subtitle":6,"date":7,"metaTitle":8,"metaDescription":9,"excerpt":10,"readingMinutes":11,"tags":12,"toc":17,"body":33},"fuenf-jahre-bestellhistorie-ins-crm","Datenmigration ins CRM: fünf Jahre Bestellhistorie, keine Dublette","Wie wir einen Import so bauen, dass man ihn zweimal laufen lassen kann – und warum ein Archiv-Flag der wichtigste Teil war","2026-09-01","Datenmigration ins CRM: ohne eine einzige Dublette","Migration einer Shop-Historie in ein CRM auf PostgreSQL: idempotenter Import, Abgleich über E-Mail und Telefon, Archiv-Flag gegen Mailings an Altkunden.","Im CRM eines Lebensmittelherstellers mit Onlineshop standen 3 814 Bestellungen, im Shop lagen fünf Jahre Historie. Wir beschreiben, wie der Import idempotent wurde, wie Kontakte über E-Mail und Telefon zusammengeführt wurden und warum ein einfaches Archiv-Flag verhindert hat, dass die Automatik Altkunden anschreibt.",5,[13,14,15,16],"Migration","CRM","PostgreSQL","Praxisbericht",[18,21,24,27,30],{"id":19,"text":20},"ausgangslage","Die Lücke: 3 814 von 19 686",{"id":22,"text":23},"risiken","Drei Dinge, die bei einer Datenmigration ins CRM brechen",{"id":25,"text":26},"idempotenz","Derselbe Importer, derselbe Schlüssel",{"id":28,"text":29},"archiv","Dubletten und das Archiv-Flag",{"id":31,"text":32},"ergebnis","Was eine Datenmigration ins CRM am Ende beweisen muss","\u003Cp>Eine Datenmigration ins CRM ist kein Kopiervorgang, sondern ein Eingriff: Im CRM eines Lebensmittelherstellers standen 3 814 Bestellungen, im Onlineshop lagen fünf Jahre Historie. Wie wir die Lücke geschlossen haben, ohne eine doppelte Kundenkarte und ohne dass ein Kunde von vor drei Jahren plötzlich Post bekam, steht hier.\u003C\u002Fp>\n\n\u003Ch2 id=\"ausgangslage\">Die Lücke: 3 814 von 19 686\u003C\u002Fh2>\n\u003Cp>Der Kunde führt Kontakte, Bestellungen und Kundenkommunikation in einem CRM, das wir für ihn betreiben. Der Shop liefert seine Bestellungen täglich über die REST-Schnittstelle in die PostgreSQL-Datenbank dieses CRM. Das funktionierte – aber erst ab dem Tag, an dem der Shop angeschlossen wurde. Beim Anschluss eines Shops kommt der Bestand nicht automatisch mit: Neue Bestellungen laufen ein, alte bleiben, wo sie sind.\u003C\u002Fp>\n\u003Cp>Es störte in dem Moment, in dem der Inhaber Fragen an seine Daten stellte. Welche Produkte verkaufen sich über Jahre, welche nur in einer Saison? Welche Kunden kommen wieder, und in welchem Abstand? Auch die KI-Assistenten, die auf den CRM-Daten arbeiten, kannten nur den Ausschnitt seit dem Anschluss. Der Wunsch war klar: alles nachladen. Die Frage war, wie man das tut, ohne den laufenden Betrieb zu beschädigen.\u003C\u002Fp>\n\n\u003Ch2 id=\"risiken\">Drei Dinge, die bei einer Datenmigration ins CRM brechen\u003C\u002Fh2>\n\u003Cp>Bevor wir eine Zeile geschrieben haben, haben wir aufgeschrieben, was schiefgehen kann. Bei Bestandsimporten in ein lebendes System sind es fast immer dieselben drei Punkte.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Dubletten.\u003C\u002Fstrong> Eine Kundin, die einmal mit einer Adresse und später mit einer anderen Schreibweise derselben E-Mail bestellt hat, taucht zweimal auf. Über fünf Jahre summiert sich das zu Karten, die niemand mehr zusammenführt.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Geweckte Automatik.\u003C\u002Fstrong> Das CRM reagiert auf neue Bestellungen: Es legt Vorgänge in der Antwortschlange an, eröffnet Verkaufschancen, löst Mailings aus. Ein Import, der für das System wie ein normaler Tag aussieht, aber die Bestellungen von fünf Jahren bringt, erzeugt ebenso viele Reaktionen – an Menschen, deren letzte Bestellung lange zurückliegt.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Falsche Zuordnung.\u003C\u002Fstrong> Der Kunde führt zwei Geschäftsbereiche: den Endkundenshop und eine Produktion für Geschäftskunden, deren Waren teilweise auch im Shop verkauft werden. Eine Bestellung gehört zum einen oder zum anderen Bereich – je nach Inhalt, nicht nach Herkunft. Wer alles auf den Shop bucht, verfälscht die Auswertung beider Bereiche.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Alle drei Punkte haben eines gemeinsam: Sie fallen nicht beim Import auf, sondern später – in einer Auswertung, in einer Beschwerde, in einer Statistik, die nicht stimmt. Deshalb haben wir sie vorab entschieden, statt sie nachträglich zu reparieren.\u003C\u002Fp>\n\n\u003Ch2 id=\"idempotenz\">Derselbe Importer, derselbe Schlüssel\u003C\u002Fh2>\n\u003Cp>Die erste Entscheidung: kein Sonderskript. Es ist verlockend, für einen einmaligen Import ein schnelles Programm zu schreiben, das die Daten irgendwie hineinschiebt. Das Programm läuft einmal, wird nie wieder angefasst, und später weiß niemand mehr, welche Sonderregeln darin steckten. Stattdessen haben wir den Importer erweitert, der den Shop ohnehin täglich anbindet. Alles, was für die Historie galt, gilt seitdem auch im Alltag – und wird jeden Tag mitgetestet.\u003C\u002Fp>\n\u003Cp>Die zweite Entscheidung: Der Import muss idempotent sein. Ein zweiter Lauf über dieselben Daten darf nichts verändern. Das klingt selbstverständlich, entscheidet aber über die Nerven aller Beteiligten. Ein Import, der nach einem Abbruch nicht neu gestartet werden kann, weil er sonst alles doppelt anlegt, zwingt zu Aufräumarbeiten in der Datenbank unter Zeitdruck. Einen idempotenten Import startet man einfach noch einmal.\u003C\u002Fp>\n\u003Cp>Technisch heißt das: Der Schlüssel einer Bestellung ist die Bestellnummer des Shops, nicht eine intern vergebene ID. Existiert eine Bestellung mit dieser Nummer bereits, wird sie aktualisiert und nicht neu angelegt. Dasselbe Prinzip gilt für Positionen und Zahlungen. Damit ließ sich der Import in Abschnitten fahren, nach einem Fehler wieder aufnehmen und am Ende ein zweites Mal komplett laufen lassen – als Nachweis, dass er nichts mehr verändert.\u003C\u002Fp>\n\n\u003Ch2 id=\"archiv\">Dubletten und das Archiv-Flag\u003C\u002Fh2>\n\u003Cp>Für Kontakte reicht die Bestellnummer nicht, denn ein Kunde hat viele Bestellungen. Hier ordnen wir über zwei Merkmale zu: E-Mail-Adresse und Telefonnummer, beide vor dem Vergleich normalisiert – Kleinschreibung, keine Leerzeichen, einheitliche Ländervorwahl. Findet sich ein bestehender Kontakt, wird seine Karte ergänzt statt verdoppelt. Die Reihenfolge der Prüfung ist dabei nicht beliebig: erst E-Mail, dann Telefon. Zwei Personen in einem Haushalt teilen sich eher eine Telefonnummer als ein Postfach.\u003C\u002Fp>\n\u003Cp>Den größten Schaden hätte allerdings die Automatik angerichtet. Unsere Lösung ist unspektakulär: Jede historische Bestellung trägt die Markierung „Archiv“. Sie erscheint in der Kundenkarte, in jeder Auswertung, in jeder Suche. Aber alle Stellen im CRM, die auf eine neue Bestellung reagieren – die Antwortschlange, die Eröffnung einer Verkaufschance, der Versand einer Bestätigung –, prüfen dieses Flag und bleiben still.\u003C\u002Fp>\n\u003Cp>Das Flag hat einen eigenen automatisierten Test, der genau diese Stellen durchspielt, und er läuft bei jeder Änderung am CRM mit. Der Grund: Eine neue Automatik, die jemand später hinzufügt, kennt die Geschichte des Imports nicht. Der Test kennt sie.\u003C\u002Fp>\n\n\u003Ch2 id=\"ergebnis\">Was eine Datenmigration ins CRM am Ende beweisen muss\u003C\u002Fh2>\n\u003Cp>Nach dem Lauf standen 19 686 Bestellungen aus fünf Jahren im CRM, keine einzige Dublette, keine ausgelöste Reaktion der Automatik. Genau diese drei Aussagen hatten wir vorher als Kriterium der Abnahme festgehalten – nicht „der Import ist gelaufen“, sondern was danach in der Datenbank nachweisbar sein muss. Der Inhaber hat damit erstmals fünf Jahre Nachfrage als Datenbasis, und die KI-Assistenten arbeiten auf der ganzen Geschichte statt auf einem Ausschnitt.\u003C\u002Fp>\n\u003Cp>Ehrlich bleibt eine Einschränkung: Die Zuordnung zum Geschäftsbereich haben wir über den Inhalt der Bestellung gelöst, und diese Regel ist eine Vereinbarung, keine Wahrheit. Ändert der Kunde sein Sortiment, muss sie nachgeführt werden. Wir haben sie deshalb an einer Stelle im Code hinterlegt und dokumentiert, statt sie über den Importer zu verteilen.\u003C\u002Fp>\n\u003Cp>Was wir aus dem Projekt mitgenommen haben, passt in einen Satz: Ein Bestandsimport ist keine Datenübertragung, sondern ein Eingriff in ein laufendes System, und die Arbeit steckt in den Regeln, nicht im Kopieren. Wie wir solche Importe und Anbindungen planen, steht unter \u003Ca href=\"\u002Fleistungen\u002Fprozessautomatisierung\">Prozessautomatisierung für den Mittelstand\u003C\u002Fa>; welche Altlasten dabei sichtbar werden, beschreibt der Artikel über \u003Ca href=\"\u002Flexikon\u002Ftechnische-schulden\">technische Schulden\u003C\u002Fa>.\u003C\u002Fp>",[35,63,71],{"slug":36,"title":37,"subtitle":38,"date":39,"metaTitle":40,"metaDescription":41,"excerpt":42,"readingMinutes":11,"tags":43,"toc":47},"senior-vakanz-bleibt-offen","Softwareentwicklung: Unternehmen mit offener Senior-Stelle haben drei Wege","Weitersuchen, Freelancer oder Werkvertrag mit einem eigenen Team – was jeder Weg kann und was nicht","2026-09-04","Senior-Stelle bleibt offen: drei Wege statt Warten","Die Senior-Stelle in der Softwareentwicklung bleibt offen, Reviews stauen sich: drei Wege für Unternehmen in der DACH-Region und die Frage, wann welcher passt.","Ein Entwicklungsteam mit einer Handvoll Leuten, eine unbesetzte Senior-Stelle, Pull Requests, die auf Reviews warten. Wir vergleichen drei Wege – weitersuchen, ein Freelancer für ein abgegrenztes Thema, ein Werkvertrag mit einem eigenen Team – und schreiben auf, wann welcher passt. Einer davon ist unser Geschäft; die anderen beiden sind oft trotzdem die richtige Antwort.",[44,45,46],"Arbeitsweise","Werkvertrag","Engineering-Management",[48,51,54,57,60],{"id":49,"text":50},"kosten","Softwareentwicklung: Unternehmen zahlen die offene Stelle doppelt",{"id":52,"text":53},"weg-1","Weg 1: weitersuchen – aber anders",{"id":55,"text":56},"weg-2","Weg 2: ein Freelancer für ein abgegrenztes Thema",{"id":58,"text":59},"weg-3","Weg 3: Werkvertrag mit einem eigenen Team",{"id":61,"text":62},"entscheidung","Softwareentwicklung – Unternehmen entscheiden nach der Aufgabe",{"slug":4,"title":5,"subtitle":6,"date":7,"metaTitle":8,"metaDescription":9,"excerpt":10,"readingMinutes":11,"tags":64,"toc":65},[13,14,15,16],[66,67,68,69,70],{"id":19,"text":20},{"id":22,"text":23},{"id":25,"text":26},{"id":28,"text":29},{"id":31,"text":32},{"slug":72,"title":73,"subtitle":74,"date":75,"metaTitle":76,"metaDescription":77,"excerpt":78,"readingMinutes":11,"tags":79,"toc":82},"ein-stack-vierzig-services","Ein Software-Stack für vierzig Services: warum wir dabei bleiben","Was ein fester Stack dem Auftraggeber bringt, was er uns kostet – und wo wir davon abweichen","2026-08-25","Ein Software-Stack für vierzig Services","Ein Software-Stack für über vierzig Services: TypeScript, Node.js, PostgreSQL. Was Auftraggeber in der DACH-Region von dieser Entscheidung haben.","Über vierzig Services laufen bei uns auf derselben Kombination aus TypeScript, Nuxt 4, Node.js und PostgreSQL mit pgvector. Wir schreiben auf, warum ein Entwickler dadurch am ersten Tag in einem fremden Projekt arbeiten kann, was ein Auftraggeber konkret davon hat – und in welchen drei Fällen wir selbst zu einer anderen Grundlage raten.",[80,81,15,44],"Architektur","TypeScript",[83,85,88,91,94],{"id":61,"text":84},"Ein Software-Stack für alles: die Entscheidung dahinter",{"id":86,"text":87},"einarbeitung","Was Einarbeitung an einem Tag konkret heißt",{"id":89,"text":90},"auftraggeber","Was Auftraggeber von einem einheitlichen Software-Stack haben",{"id":92,"text":93},"postgres","Warum PostgreSQL für fast alles reicht",{"id":95,"text":96},"grenzen","Wo eine andere Grundlage die bessere Antwort ist",1789404874646]