# Beteiligte Sync - Architektur-Analyse **Stand:** 7. Februar 2026 **Analysiert:** Bidirektionale EspoCRM ↔ Advoware Beteiligte-Synchronisation --- ## πŸ—οΈ ARCHITEKTUR-ÜBERSICHT ### Komponenten ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ EspoCRM (Master) β”‚ β”‚ Webhooks β†’ Motia β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ WEBHOOK HANDLER (3 Endpoints) β”‚ β”‚ β€’ beteiligte_create_api_step.py β”‚ β”‚ β€’ beteiligte_update_api_step.py ← Loop-Prevention entfernt β”‚ β”‚ β€’ beteiligte_delete_api_step.py β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ emits events β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CENTRAL SYNC HANDLER (Event-Based) β”‚ β”‚ beteiligte_sync_event_step.py (~329 lines) β”‚ β”‚ β”‚ β”‚ Subscribes: vmh.beteiligte.{create,update,delete,sync_check} β”‚ β”‚ β”‚ β”‚ Flow: β”‚ β”‚ 1. Distributed Lock (Redis + syncStatus) β”‚ β”‚ 2. Fetch EspoCRM Entity β”‚ β”‚ 3. Route: CREATE, UPDATE/CHECK, DELETE β”‚ β”‚ 4. Release Lock + Update Status β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ handle_create β”‚ β”‚ handle_update β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β€’ Map to Advo β”‚ β”‚ β€’ Fetch Advo β”‚ β”‚ β€’ POST β”‚ β”‚ β€’ Compare β”‚ β”‚ β€’ GET rowId β”‚ β”‚ β€’ Sync/Skip β”‚ β”‚ β€’ Write back β”‚ β”‚ β€’ Update rowId β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SUPPORT SERVICES β”‚ β”‚ β”‚ β”‚ β€’ BeteiligteSync (sync_utils) (~524 lines) β”‚ β”‚ - Locking, Compare, Merge, Conflict Resolution β”‚ β”‚ β”‚ β”‚ β€’ BeteiligteMapper (~200 lines) β”‚ β”‚ - EspoCRM ↔ Advoware transformations β”‚ β”‚ - None-value filtering β”‚ β”‚ - Date format conversion β”‚ β”‚ β”‚ β”‚ β€’ AdvowareAPI / EspoCRMAPI β”‚ β”‚ - HTTP clients mit Token-Caching β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## βœ… STΓ„RKEN (Was funktioniert gut) ### 1. **Robustheit durch Distributed Locking** ```python # 2-stufiges Locking verhindert Race Conditions: # 1. Redis Lock (atomic, TTL 15min) # 2. syncStatus Update (UI visibility) lock_key = f"sync_lock:cbeteiligte:{entity_id}" acquired = self.redis.set(lock_key, "locked", nx=True, ex=LOCK_TTL_SECONDS) ``` βœ… **Gut:** Verhindert parallele Syncs derselben Entity βœ… **Gut:** TTL verhindert Deadlocks bei Crashes βœ… **Gut:** UI-Sichtbarkeit via syncStatus ### 2. **PrimΓ€re Change Detection: rowId** ```python # rowId Γ€ndert sich bei JEDEM Advoware PUT β†’ sehr zuverlΓ€ssig if espo_rowid and advo_rowid: advo_changed = (espo_rowid != advo_rowid) espo_changed = (espo_modified > last_sync) ``` βœ… **Sehr gut:** rowId ist Base64, Γ€ndert sich immer, keine NULLs βœ… **Gut:** Timestamp als Fallback vorhanden βœ… **Gut:** Konfliktlogik (beide geΓ€ndert) implementiert ### 3. **API-Call-Optimierung (50% Reduktion)** ```python # VORHER: PUT + GET (2 Calls) # NACHHER: PUT Response enthΓ€lt neue rowId (1 Call) put_result = await advoware.api_call(...) new_rowid = put_result[0].get('rowId') # direkt aus Response! ``` βœ… **Exzellent:** Keine extra GETs nach PUT nΓΆtig βœ… **Gut:** Funktioniert fΓΌr CREATE, UPDATE, Conflict Resolution ### 4. **Loop-Prevention auf EspoCRM-Seite** ```python # ENTFERNT: should_skip_update() Filterung # NEU: EspoCRM triggert keine Webhooks fΓΌr rowId-Updates ``` βœ… **Gut:** Vereinfacht Code erheblich βœ… **Gut:** Keine komplexe Filterlogik mehr nΓΆtig βœ… **Gut:** Vertraut auf EspoCRM-Konfiguration ### 5. **Mapper mit Validierung** ```python # None-Filtering verhindert EspoCRM Validation Errors espo_data = {k: v for k, v in espo_data.items() if v is not None} # Datumsformat-Konversion dateOfBirth = geburtsdatum.split('T')[0] # '2001-01-05T00:00:00' β†’ '2001-01-05' ``` βœ… **Gut:** Robuste Fehlerbehandlung βœ… **Gut:** Dokumentiert welche Felder funktionieren (nur 8 von 14) ### 6. **Error Handling mit Retry-Logik** ```python MAX_SYNC_RETRIES = 5 if new_retry >= MAX_SYNC_RETRIES: update_data['syncStatus'] = 'permanently_failed' await self.send_notification(...) ``` βœ… **Gut:** Verhindert Endlos-Retries βœ… **Gut:** Notification an User bei dauerhaftem Fehler --- ## πŸ”΄ SCHWΓ„CHEN & VERBESSERUNGSPOTENZIALE ### 1. **CREATE-Flow ineffizient (Extra GET nach POST)** **Problem:** ```python # Nach POST: Extra GET nur fΓΌr rowId result = await advoware.api_call(..., method='POST', data=advo_data) new_betnr = result.get('betNr') # Extra GET! created_entity = await advoware.api_call(f'.../{new_betnr}', method='GET') new_rowid = created_entity.get('rowId') ``` **LΓΆsung:** ```python # POST Response sollte rowId bereits enthalten (prΓΌfen!) # Falls ja: Extrahiere direkt wie bei PUT if isinstance(result, dict): new_rowid = result.get('rowId') elif isinstance(result, list): new_rowid = result[0].get('rowId') ``` ⚠️ **TODO:** Teste ob Advoware POST auch rowId zurΓΌckgibt (wie PUT) --- ### 2. **Doppeltes rowId-Update nach EspoCRMβ†’Advoware** **Problem:** ```python # 1. Update via release_sync_lock extra_fields await sync_utils.release_sync_lock(entity_id, 'clean', extra_fields={'advowareRowId': new_rowid}) # 2. Aber VORHER bereits direktes Update! if new_rowid: await espocrm.update_entity('CBeteiligte', entity_id, { 'advowareRowId': new_rowid }) ``` **LΓΆsung:** Entweder/oder - nicht beides! ```python # OPTION A: Nur release_lock (weniger Code, eleganter) await sync_utils.release_sync_lock( entity_id, 'clean', extra_fields={'advowareRowId': new_rowid} ) # OPTION B: Direktes Update + release ohne extra_fields await espocrm.update_entity('CBeteiligte', entity_id, { 'advowareRowId': new_rowid, 'syncStatus': 'clean', 'advowareLastSync': now() }) await self.redis.delete(lock_key) ``` ⚠️ **Recommendation:** Option A ist eleganter (1 API Call statt 2) --- ### 3. **Initial Sync Special Case nicht nΓΆtig?** **Problem:** ```python # Separate Logik fΓΌr "kein lastSync" if not espo_entity.get('advowareLastSync'): context.logger.info(f"πŸ“€ Initial Sync β†’ ...") # ... exakt derselbe Code wie bei espocrm_newer! ``` **LΓΆsung:** compare_entities sollte das automatisch erkennen ```python # In compare_entities: if not last_sync: # Kein lastSync β†’ EspoCRM neuer (always sync on first run) return 'espocrm_newer' ``` βœ… **Eliminiert:** ~30 Zeilen Duplikat-Code in event_step.py --- ### 4. **Conflict Resolution immer EspoCRM Wins** **Problem:** ```python # Hardcoded: EspoCRM gewinnt immer elif comparison == 'conflict': context.logger.warn(f"⚠️ KONFLIKT erkannt β†’ EspoCRM WINS") # ... force update zu Advoware ``` **Überlegungen:** - FΓΌr **STAMMDATEN** ist das sinnvoll (EspoCRM ist Master) - FΓΌr **Kontaktdaten** kΓΆnnte Advoware Master sein - FΓΌr **Adresse** sollte vielleicht Merge stattfinden βœ… **Status:** OK fΓΌr aktuelle Scope (nur Stammdaten) πŸ“ **SpΓ€ter:** Konfigurierbare Conflict Strategy pro Feld-Gruppe --- ### 5. **Timestamp-Fallback verwendet geaendertAm (deprecated?)** **Code:** ```python return self.compare_timestamps( espo_entity.get('modifiedAt'), advo_entity.get('geaendertAm'), # ← Swagger deprecated? espo_entity.get('advowareLastSync') ) ``` ⚠️ **TODO:** PrΓΌfe ob `geaendertAm` zuverlΓ€ssig ist oder ob Advoware ein anderes Feld hat --- ### 6. **Keine Batch-Verarbeitung fΓΌr Webhooks** **Problem:** ```python # Webhook-Handler: Emittiert Event pro Entity for entity_id in entity_ids: await context.emit({...}) # N Events ``` **Resultat:** Bei 100 Updates β†’ 100 separate Event-Handler-Invocations **LΓΆsung (Optional):** ```python # Batch-Event mit allen IDs await context.emit({ 'topic': 'vmh.beteiligte.update_batch', 'data': { 'entity_ids': list(entity_ids), # Alle auf einmal 'source': 'webhook' } }) # Handler verarbeitet in Parallel (mit Limit) async def handler_batch(event_data, context): entity_ids = event_data['entity_ids'] # Process max 10 parallel semaphore = asyncio.Semaphore(10) tasks = [sync_with_semaphore(id, semaphore) for id in entity_ids] await asyncio.gather(*tasks) ``` πŸ“ **Entscheidung:** Aktuell OK (Lock verhindert Probleme), aber bei >50 gleichzeitigen Updates kΓΆnnte Batch helfen --- ### 7. **Fehlende Metriken/Monitoring** **Was fehlt:** - Durchschnittliche Sync-Dauer pro Entity - Anzahl Konflikte pro Tag - API-Call-Count (EspoCRM vs Advoware) - Failed Sync Ratio **LΓΆsung:** ```python # In sync_utils oder neues monitoring_utils.py class SyncMetrics: async def record_sync(self, entity_id, duration, result, comparison): await redis.hincrby('metrics:sync:daily', 'total', 1) await redis.hincrby('metrics:sync:daily', f'result_{result}', 1) await redis.lpush('metrics:sync:durations', duration) ``` --- ## 🎯 VERBESSERUNGS-EMPFEHLUNGEN ### PrioritΓ€t 1: SOFORT (Effizienz) 1. **βœ… Eliminiere doppeltes rowId-Update** ```python # NUR in release_sync_lock, nicht vorher extra Update ``` Impact: -1 API Call pro EspoCRMβ†’Advoware Update (ca. 50% weniger EspoCRM calls) 2. **βœ… Teste POST Response fΓΌr rowId** ```python # Falls POST auch rowId enthΓ€lt: Extra GET entfernen ``` Impact: -1 API Call pro CREATE (50% weniger bei Neuanlagen) ### PrioritΓ€t 2: MITTELFRISTIG (Eleganz) 3. **πŸ“ Merge Initial Sync in compare_entities** ```python # Eliminiert Special Case, -30 Zeilen ``` Impact: Cleaner Code, leichter wartbar 4. **πŸ“ PrΓΌfe geaendertAm Timestamp** ```python # Stelle sicher dass Fallback funktioniert ``` Impact: Robustheit falls rowId mal fehlt ### PrioritΓ€t 3: OPTIONAL (Features) 5. **πŸ’‘ Batch-Processing fΓΌr Webhooks** - Bei >50 gleichzeitigen Updates kΓΆnnte Performance leiden - Aktuell nicht kritisch (Lock verhindert Probleme) 6. **πŸ’‘ Metriken/Dashboard** - Sync-Statistiken fΓΌr Monitoring - Nicht kritisch aber nΓΌtzlich fΓΌr Ops --- ## πŸ“Š PERFORMANCE-SCHΓ„TZUNG ### Aktueller Stand (pro Entity) **CREATE:** - 1Γ— EspoCRM GET (Entity laden) - 1Γ— Advoware POST - 1Γ— Advoware GET (rowId holen) ← **OPTIMIERBAR** - 1Γ— EspoCRM PUT (betNr + rowId schreiben) ← **OPTIMIERBAR** - 1Γ— EspoCRM PUT (syncStatus + lastSync) ← Teil von Lock-Release **Total: 5 API Calls** β†’ Mit Optimierung: **3 API Calls (-40%)** **UPDATE (EspoCRMβ†’Advoware):** - 1Γ— EspoCRM GET (Entity laden) - 1Γ— Advoware GET (Vergleich) - 1Γ— Advoware PUT - 1Γ— EspoCRM PUT (rowId update) ← **DOPPELT** - 1Γ— EspoCRM PUT (Lock-Release mit rowId) ← **DOPPELT** **Total: 5 API Calls** β†’ Mit Optimierung: **4 API Calls (-20%)** **UPDATE (Advowareβ†’EspoCRM):** - 1Γ— EspoCRM GET - 1Γ— Advoware GET - 1Γ— EspoCRM PUT (Daten) - 1Γ— EspoCRM PUT (Lock-Release) **Total: 4 API Calls** β†’ Bereits optimal --- ## 🎨 ARCHITEKTUR-BEWERTUNG ### βœ… Was ist ROBUST? 1. **Distributed Locking** - Verhindert Race Conditions 2. **rowId Change Detection** - Sehr zuverlΓ€ssig 3. **Retry Logic** - Graceful Degradation 4. **Error Handling** - Try-catch auf allen Ebenen 5. **TTL auf Locks** - Keine Deadlocks ### βœ… Was ist EFFIZIENT? 1. **PUT Response Parsing** - Spart GET nach Updates 2. **None-Filtering** - Verhindert unnΓΆtige Validierungsfehler 3. **Early Returns** - "no_change" skipped sofort 4. **Redis Token Caching** - Nicht bei jedem Call neu authentifizieren ### βœ… Was ist ELEGANT? 1. **Event-Driven Architecture** - Entkoppelt Webhook von Sync-Logik 2. **Mapper Pattern** - Transformationen zentral 3. **Utility Class** - Wiederverwendbare Funktionen 4. **Descriptive Logging** - Mit Emojis, sehr lesbar ### ⚠️ Was kΓΆnnte ELEGANTER sein? 1. **Doppelte rowId-Updates** - Redundant 2. **Initial Sync Special Case** - UnnΓΆtige Duplikation 3. **Keine Config fΓΌr Conflict Strategy** - Hardcoded 4. **Fehlende Metriken** - Monitoring schwierig --- ## πŸ† GESAMTBEWERTUNG | Kategorie | Bewertung | Note | |-----------|-----------|------| | Robustheit | ⭐⭐⭐⭐⭐ | 9/10 - Sehr stabil durch Locking + Retry | | Effizienz | β­β­β­β­β˜† | 7/10 - Gut, aber 2 klare Optimierungen mΓΆglich | | Eleganz | β­β­β­β­β˜† | 8/10 - Sauber strukturiert, kleine Code-Duplikate | | Wartbarkeit | ⭐⭐⭐⭐⭐ | 9/10 - Gut dokumentiert, klare Struktur | | Erweiterbarkeit | β­β­β­β­β˜† | 8/10 - Event-Driven macht Extensions einfach | **Gesamt: 8.2/10 - SEHR GUT** --- ## πŸš€ EMPFOHLENE NΓ„CHSTE SCHRITTE ### Sofort (1-2h Aufwand): 1. βœ… Eliminiere doppeltes rowId-Update (5min) 2. βœ… Teste Advoware POST Response auf rowId (15min) 3. βœ… Falls ja: Entferne GET nach CREATE (5min) ### Mittelfristig (2-4h): 4. πŸ“ Merge Initial Sync in compare_entities (30min) 5. πŸ“ Add Metrics Collection (1-2h) ### Optional: 6. πŸ’‘ Batch-Processing (nur wenn Performance-Problem) 7. πŸ’‘ Configurable Conflict Strategy (bei neuen Requirements) --- ## πŸ“ FAZIT **Das System ist produktionsreif und robust.** - **StΓ€rken:** Exzellentes Locking, zuverlΓ€ssige Change Detection, gutes Error Handling - **Optimierungen:** 2-3 kleine Fixes kΓΆnnen 20-40% API Calls sparen - **Architektur:** Sauber, wartbar, erweiterbar **Recommendation:** Ship it mit den 2 Quick-Fixes (doppeltes Update + POST rowId-Check).