Integration

Legacy-ERP-Integration mit Azure Functions: Lessons Learned

Wie eine klassische SFTP/XML-basierte ERP-Datenlieferung durch moderne Serverless-Architektur transformiert wurde - mit Fokus auf Orchestrierung, Fehlerbehandlung und Performance-Optimierung.

📖 16 Min. Lesezeit
Azure Functions ERP Integration Serverless .NET Legacy Systems

Legacy-ERP-Integration mit Azure Functions: Lessons Learned

Integrationsprojekte mit Legacy-ERP-Systemen sind selten glamourös, aber oft geschäftskritisch. Als ich vor zwei Jahren das Projekt übernahm, einen klassischen ERP-Datenexport in eine moderne E-Commerce-Plattform zu integrieren, war die Ausgangslage typisch für viele Enterprise-Szenarien: Ein SAP-ERP-System liefert nachts XML-Dateien via SFTP, diese müssen transformiert, mit Produkt-Bildern und generierten PDFs angereichert, und an einen Magento-Shop via REST-API übergeben werden. Klingt simpel, ist es aber nie.

Die bestehende Lösung – ein monolithischer Windows-Service auf einem dedicated Server – hatte zwei kritische Probleme: Sie war fragil (jeder vierte Import schlug fehl) und skalierte nicht (Black-Friday-Datenvolumen brachte den Server zum Absturz). Das Business-Requirement war klar: 99.9% Erfolgsrate, variable Skalierung, und drastisch reduzierte Betriebskosten.

Die Lösung, die ich architected habe, nutzte Azure Functions in einer event-driven, serverless Microservice-Architektur. Nach 14 Monaten in Production verarbeitet sie täglich 50.000-200.000 Produktupdates mit 99.97% Erfolgsrate, skaliert automatisch auf Last-Spitzen, und kostet 70% weniger als die alte Lösung. Dieser Artikel teilt die architektonischen Entscheidungen, technischen Herausforderungen, und gelernten Lektionen dieses Projekts.

Die Ausgangslage: Typisches Legacy-Integration-Szenario

Das alte System

Legacy-Integration-Architektur (2021):

[SAP ERP] →→ [SFTP Server] →→ [Windows Service] →→ [Magento API]

Windows Service (Monolith):
├── Runs on dedicated VM (Standard_D4s_v3, €200/Monat)
├── Scheduled Task: 02:00 Uhr täglich
├── Process:
│   1. Poll SFTP für neue XML-Dateien
│   2. Download und XML-Parsing
│   3. Bild-Download von separatem System
│   4. PDF-Generation (Datenblätter, Zertifikate)
│   5. Magento API-Calls (sequenziell!)
│   6. Logging zu lokaler Datei
│   └── Duration: 4-8 Stunden

Probleme:
├── Fehlerrate: 23% (jeder 4.-5. Lauf schlug fehl)
├── Error-Handling: Minimal, oft manuelle Intervention nötig
├── Skalierung: Fix auf VM-Size, Black Friday = Chaos
├── Monitoring: Logs auf VM, kein Alerting
├── Deployment: RDP, manual file copy, service restart
├── Cost: €200/Monat VM + €80/Monat Ops-Zeit
└── Business-Risk: Single Point of Failure

Typische Fehler-Szenarien:
├── SFTP-Connection Timeout → Gesamter Import failed
├── Malformed XML → Exception, Import stopped
├── Magento API-Rate-Limit → 429 errors, Import incomplete
├── Out of Memory bei großen Dateien (> 500MB XML)
└── Disk-Space exhausted durch PDFs

Die Business-Requirements

Stakeholder-Anforderungen:

1. Zuverlässigkeit (Geschäftsführung)
   ├── 99.9% Erfolgsrate
   ├── Automatisches Retry bei Fehlern
   └── Alerting bei kritischen Problemen

2. Skalierbarkeit (Operations)
   ├── Black Friday: 10x normales Volumen
   ├── Flash-Sales: Spontane Produkt-Uploads
   └── Keine manuelle Intervention für Skalierung

3. Cost-Efficiency (CFO)
   ├── Reduzierte Infrastruktur-Kosten
   ├── Pay-per-Use Modell
   └── Minimaler Ops-Overhead

4. Visibility (IT-Management)
   ├── Real-time Status-Dashboard
   ├── Historische Metriken
   └── Proaktive Fehler-Detection

5. Flexibilität (Productmanagement)
   ├── Schnelle Anpassungen an XML-Format-Änderungen
   ├── A/B-Testing für Integrations-Logik
   └── Einfaches Rollback bei Problemen

Die Lösung: Event-Driven Serverless Architecture

Architektur-Overview

Neue Architektur (Azure Functions + Event-Driven):

[SAP ERP]
    ↓ (XML via SFTP, nächtlich)
[Azure Blob Storage] (Landing Zone)
    ↓ (Blob Created Event)
[Function: FileDetector]
    ↓ (Queue Message)
[Function: XMLParser]
    ↓ (Queue Messages: Individual Products)
┌───────────┬──────────┬──────────┐
│  Function:│ Function:│ Function:│
│  ImageGen │  PDFGen  │ Enricher │
└───────────┴──────────┴──────────┘
    ↓ (Queue: Enriched Products)
[Function: MagentoPublisher]
    ↓ (Magento REST API)
[Magento Shop]

Support-Services:
├── Azure SQL Database (State Tracking, Audit Logs, Reporting)
├── Application Insights (Monitoring, Logging)
├── Azure Queue Storage (Message Bus)
├── Azure Service Bus (Dead-Letter-Queue)
└── Azure Key Vault (Credentials, API-Keys)

Detaillierte Komponenten-Beschreibung

1. FileDetector Function (Blob Trigger)

public class FileDetectorFunction
{
    private readonly ILogger<FileDetectorFunction> _logger;
    private readonly IQueueClient _queueClient;

    [FunctionName("FileDetector")]
    public async Task Run(
        [BlobTrigger("erp-imports/{name}", Connection = "StorageConnection")] Stream myBlob,
        string name,
        ILogger log)
    {
        _logger.LogInformation($"Detected new file: {name}, Size: {myBlob.Length} bytes");

        // Validation
        if (!name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
        {
            _logger.LogWarning($"Skipping non-XML file: {name}");
            return;
        }

        // File size check (Azure Functions haben 1.5GB memory limit)
        if (myBlob.Length > 500 * 1024 * 1024) // 500MB
        {
            _logger.LogError($"File too large: {name}, {myBlob.Length} bytes");
            await SendAlertAsync($"File {name} exceeds size limit");
            return;
        }

        // Enqueue for parsing
        var message = new FileDetectedMessage
        {
            BlobName = name,
            FileSize = myBlob.Length,
            DetectedAt = DateTime.UtcNow
        };

        await _queueClient.SendMessageAsync(JsonSerializer.Serialize(message));

        _logger.LogInformation($"File {name} queued for processing");
    }
}

Lessons Learned:

  • Blob Trigger ist asynchron: File detection passiert innerhalb von Sekunden, aber nicht instantan
  • Size Limits beachten: Functions haben Memory-Limits, große Dateien brauchen Chunking
  • Idempotenz: Blob-Trigger kann mehrfach feuern (selten, aber möglich), State-Tracking ist kritisch

2. XMLParser Function (Queue Trigger)

Die XMLParser-Function ist das Herzstück der Datenverarbeitung und stellt besondere Anforderungen an Speicher-Effizienz und Fehlertoleranz. Anstatt XML-Dokumente vollständig in den Speicher zu laden, verwenden wir Stream-basiertes Parsing, das auch mehrere Gigabyte große Dateien verarbeiten kann. Die Verwendung von SQL Server für State Tracking bietet uns dabei mehrere Vorteile gegenüber NoSQL-Alternativen: ACID-Transaktionen für konsistente Statusverfolgung, komplexe Abfragen für Reporting, und etablierte Backup-/Recovery-Mechanismen für geschäftskritische Importdaten.

public class XMLParserFunction
{
    private readonly IProductQueue _productQueue;
    private readonly ISqlRepository _stateStore;

    [FunctionName("XMLParser")]
    public async Task Run(
        [QueueTrigger("file-detected-queue", Connection = "StorageConnection")] string queueMessage,
        ILogger log)
    {
        var message = JsonSerializer.Deserialize<FileDetectedMessage>(queueMessage);

        _logger.LogInformation($"Parsing file: {message.BlobName}");

        // Download blob
        var blobClient = _blobServiceClient.GetBlobContainerClient("erp-imports")
            .GetBlobClient(message.BlobName);

        // Stream-based parsing für Memory-Efficiency
        await using var stream = await blobClient.OpenReadAsync();

        using var xmlReader = XmlReader.Create(stream, new XmlReaderSettings
        {
            Async = true,
            IgnoreWhitespace = true,
            DtdProcessing = DtdProcessing.Prohibit // Security: Prevent XXE attacks
        });

        var productCount = 0;
        var batchProducts = new List<ProductMessage>();

        while (await xmlReader.ReadAsync())
        {
            if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == "Product")
            {
                // Parse individual product
                var productXml = await xmlReader.ReadOuterXmlAsync();
                var product = ParseProduct(productXml);

                // Validate
                if (!ValidateProduct(product, out var errors))
                {
                    _logger.LogWarning($"Invalid product: {product.SKU}, Errors: {string.Join(", ", errors)}");
                    continue;
                }

                batchProducts.Add(new ProductMessage
                {
                    SKU = product.SKU,
                    Name = product.Name,
                    Price = product.Price,
                    Description = product.Description,
                    ImageUrls = product.ImageUrls,
                    PDFSpec = product.PDFSpec,
                    ImportBatch = message.BlobName,
                    ImportedAt = DateTime.UtcNow
                });

                productCount++;

                // Batch-enqueue every 100 products
                if (batchProducts.Count >= 100)
                {
                    await EnqueueProductBatch(batchProducts);
                    batchProducts.Clear();
                }
            }
        }

        // Enqueue remaining
        if (batchProducts.Any())
        {
            await EnqueueProductBatch(batchProducts);
        }

        // Track import state in SQL Server
        await _stateStore.UpsertImportStateAsync(new ImportState
        {
            ImportBatchId = message.BlobName,
            FileName = message.BlobName,
            ProductCount = productCount,
            Status = "Parsing Complete",
            StartedAt = message.DetectedAt,
            CompletedAt = DateTime.UtcNow,
            DurationSeconds = (int)(DateTime.UtcNow - message.DetectedAt).TotalSeconds
        });

        _logger.LogInformation($"Parsed {productCount} products from {message.BlobName}");
    }

    private async Task EnqueueProductBatch(List<ProductMessage> products)
    {
        var tasks = products.Select(p =>
            _productQueue.SendMessageAsync(JsonSerializer.Serialize(p)));

        await Task.WhenAll(tasks);
    }

    private Product ParseProduct(string productXml)
    {
        // XPath-based parsing, resilient to structure changes
        var doc = XDocument.Parse(productXml);

        return new Product
        {
            SKU = doc.XPathSelectElement("//SKU")?.Value ?? throw new InvalidDataException("Missing SKU"),
            Name = doc.XPathSelectElement("//Name")?.Value,
            Price = decimal.Parse(doc.XPathSelectElement("//Price")?.Value ?? "0"),
            Description = doc.XPathSelectElement("//Description")?.Value,
            ImageUrls = doc.XPathSelectElements("//ImageURL")
                .Select(e => e.Value)
                .ToList(),
            PDFSpec = new PDFSpecification
            {
                TemplateName = doc.XPathSelectElement("//PDFTemplate")?.Value,
                Data = doc.XPathSelectElement("//PDFData")?.Value
            }
        };
    }
}

Lessons Learned:

  • Stream-based parsing ist kritisch: DOM-based (XDocument.Load) lädt entire file in memory → OOM bei großen Files
  • Batch-Enqueueing: 1 Queue-Message pro Product = 50.000 Messages. Batching (100 Products) reduziert Queue-Ops um 99%
  • Schema-Changes: XPath ist robuster als strong-typed deserialization bei häufigen XML-Format-Änderungen
  • Poison Messages: Invalid XML crasht Function → Dead-Letter-Queue ist essential

3. ImageGen, PDFGen, Enricher Functions (Parallel Processing)

// Parallel ausgeführt für Performance

[FunctionName("ImageGenerator")]
public async Task GenerateImages(
    [QueueTrigger("product-queue")] string productMessage,
    [Blob("product-images/{rand-guid}.jpg")] Stream output,
    ILogger log)
{
    var product = JsonSerializer.Deserialize<ProductMessage>(productMessage);

    // Download source images
    var sourceImages = await DownloadImagesAsync(product.ImageUrls);

    // Generate composite (z.B. Produktbild + Logo + Wasserzeichen)
    using var compositeImage = ComposeImages(sourceImages);

    // Optimize (resize, compress)
    var optimized = await OptimizeImageAsync(compositeImage);

    // Upload
    await optimized.CopyToAsync(output);

    // Update product with generated image URL
    product.GeneratedImageUrl = $"https://storage.../product-images/{product.SKU}.jpg";

    await _enrichedQueue.SendMessageAsync(JsonSerializer.Serialize(product));
}

[FunctionName("PDFGenerator")]
public async Task GeneratePDF(
    [QueueTrigger("product-queue")] string productMessage,
    [Blob("product-pdfs/{rand-guid}.pdf")] Stream output,
    ILogger log)
{
    var product = JsonSerializer.Deserialize<ProductMessage>(productMessage);

    if (string.IsNullOrEmpty(product.PDFSpec?.TemplateName))
    {
        // No PDF needed, pass through
        await _enrichedQueue.SendMessageAsync(productMessage);
        return;
    }

    // Render HTML template mit product data
    var html = await RenderTemplateAsync(product.PDFSpec.TemplateName, product);

    // Convert to PDF (using iTextSharp or Puppeteer)
    using var pdfStream = await ConvertHtmlToPdfAsync(html);

    await pdfStream.CopyToAsync(output);

    product.PDFUrl = $"https://storage.../product-pdfs/{product.SKU}.pdf";

    await _enrichedQueue.SendMessageAsync(JsonSerializer.Serialize(product));
}

Lessons Learned:

  • Parallelität: Image + PDF generation passieren parallel für gleichen Product → massive Performance-Gewinn
  • Cold-Start-Problem: First invocation dauert 5-10s. Bei 50.000 products sind das potentiell Hours. Premium-Plan mit immer warmen Instances löst das (Kosten-Trade-off)
  • Memory-intensive Operations: Image/PDF-Gen braucht viel RAM. Instance-Size und Timeout-Configuration sind kritisch
  • External Dependencies: iTextSharp, ImageSharp Libraries erhöhen Deployment-Size → Deployment-Optimization nötig

4. MagentoPublisher Function (Final Integration)

public class MagentoPublisherFunction
{
    private readonly IMagentoClient _magentoClient;
    private readonly IRateLimiter _rateLimiter;

    [FunctionName("MagentoPublisher")]
    public async Task Publish(
        [QueueTrigger("enriched-products-queue")] string productMessage,
        ILogger log)
    {
        var product = JsonSerializer.Deserialize<ProductMessage>(productMessage);

        // Rate limiting (Magento API: max 20 req/sec)
        await _rateLimiter.AcquireAsync();

        try
        {
            // Check if product exists
            var existing = await _magentoClient.GetProductAsync(product.SKU);

            if (existing != null)
            {
                // Update
                await _magentoClient.UpdateProductAsync(product.SKU, new
                {
                    name = product.Name,
                    price = product.Price,
                    description = product.Description,
                    media_gallery_entries = new[]
                    {
                        new { file = product.GeneratedImageUrl, media_type = "image" }
                    },
                    custom_attributes = new[]
                    {
                        new { attribute_code = "datasheet_pdf", value = product.PDFUrl }
                    }
                });

                _logger.LogInformation($"Updated product: {product.SKU}");
            }
            else
            {
                // Create
                await _magentoClient.CreateProductAsync(new
                {
                    sku = product.SKU,
                    name = product.Name,
                    price = product.Price,
                    // ... full product data
                });

                _logger.LogInformation($"Created product: {product.SKU}");
            }

            // Track success
            await UpdateImportStatistics(product.ImportBatch, success: true);
        }
        catch (MagentoRateLimitException ex)
        {
            // 429 Too Many Requests
            _logger.LogWarning($"Rate limit hit for {product.SKU}, requeueing");

            // Requeue with delay
            await _queueClient.SendMessageAsync(
                productMessage,
                visibilityTimeout: TimeSpan.FromMinutes(5));
        }
        catch (MagentoApiException ex)
        {
            _logger.LogError(ex, $"Magento API error for {product.SKU}: {ex.Message}");

            // Track failure
            await UpdateImportStatistics(product.ImportBatch, success: false, error: ex.Message);

            // Move to dead-letter after 5 retries
            if (ex.RetryCount >= 5)
            {
                await _deadLetterQueue.SendMessageAsync(productMessage);
            }
            else
            {
                throw; // Trigger automatic retry
            }
        }
    }
}

Lessons Learned:

  • Rate Limiting ist unvermeidlich: External APIs haben Limits. Eigener Rate-Limiter (Token-Bucket) verhindert 429-Errors
  • Retry-Strategie: Exponential Backoff mit Jitter. Nicht blindes Retry → verschlimmert Overload
  • Idempotenz: Update-Operation muss mehrfach ausführbar sein ohne Side-Effects
  • Dead-Letter-Queue: Nach X Retries Poison-Messages isolieren für manuelle Investigation

Performance-Optimierung: Von 8 Stunden auf 45 Minuten

Die Performance-Optimierung war ein iterativer Prozess über mehrere Monate. Was anfangs als "funktionierender MVP" erschien, offenbarte bei Produktionslasten erhebliche Engpässe, die systematisch identifiziert und behoben werden mussten. Die Herausforderung bestand darin, die richtige Balance zwischen Durchsatz, Ressourcenverbrauch und Kosten zu finden. Besonders die Entscheidung für SQL Server als zentrales State-Tracking-System erwies sich als kritisch: Während NoSQL-Lösungen wie Azure Table Storage initial schneller erscheinen, benötigten wir für komplexe Reporting-Anfragen und transaktionale Konsistenz die Mächtigkeit einer relationalen Datenbank. SQL Server ermöglichte uns zudem, detaillierte Performance-Metriken zu erfassen und Flaschenhälse präzise zu identifizieren.

Initial Performance (MVP)

Initial Deployment (Monat 1):
50.000 Products Import-Duration: 6 Stunden

Bottlenecks identifiziert:
├── Sequential processing: Functions processed 1 product at a time
├── Cold starts: Functions starteten für jedes Product neu
├── Network latency: Jeder API-Call 200ms round-trip
├── No parallelism: ImageGen wartete auf PDFGen
└── State tracking overhead: Azure Table Storage-Latenz bei hohem Durchsatz

Optimization 1: Parallel Execution

// BEFORE: Sequential Queue Processing
// Queue: Product 1 → Process → Complete → Product 2 → ...

// AFTER: Parallel Processing via Function Scaling
// Azure Functions settings:
{
  "extensions": {
    "queues": {
      "batchSize": 32,        // Process 32 messages in parallel per instance
      "maxDequeueCount": 5,   // Max retries before dead-letter
      "newBatchThreshold": 16 // Fetch new batch when 16 messages remain
    }
  },
  "functionTimeout": "00:10:00" // 10-minute timeout
}

Result: 50.000 Products jetzt in 2 Stunden (75% Reduktion)

Optimization 2: Instance Warm-Up

// Premium Plan: Always-warm instances
// Eliminiert cold starts

// Cost: €150/Monat
// Benefit: 5-10s cold start eliminated für alle invocations
// ROI: Bei 50.000 products × 7s = 97 hours saved → Massive win

Result: 2 Stunden → 1 Stunde

Optimization 3: Batch API Calls

// BEFORE: 1 API Call per product
await _magentoClient.UpdateProductAsync(product.SKU, productData);

// AFTER: Batch API (Magento supports bulk operations)
[FunctionName("MagentoBatchPublisher")]
public async Task PublishBatch(
    [QueueTrigger("enriched-products-batch-queue")] string batchMessage,
    ILogger log)
{
    var products = JsonSerializer.Deserialize<List<ProductMessage>>(batchMessage);

    // Magento Bulk API: up to 100 products per call
    var batches = products.Chunk(100);

    foreach (var batch in batches)
    {
        await _magentoClient.BulkUpdateAsync(batch.Select(p => new
        {
            sku = p.SKU,
            name = p.Name,
            // ...
        }).ToArray());
    }
}

Result: 50.000 API calls → 500 API calls (100x Reduktion)
        1 Stunde → 45 Minuten

Final Performance

Optimized Architecture:
50.000 Products: 45 Minuten (was: 6-8 Stunden)
200.000 Products (Black Friday): 2.5 Stunden (was: System-Crash)

Cost:
├── Consumption: €50/Monat (bei 50K products/Tag)
├── Premium Plan: €150/Monat (always-warm instances)
├── Storage: €20/Monat (Blobs, Queues, Tables)
└── Total: €220/Monat (was: €280/Monat VM + Ops)

Savings: 22% direct cost, 90% Ops-Zeit, infinite scalability

Fehlerbehandlung & Resilience

Die Fehlerbehandlung stellte sich als komplexer heraus als ursprünglich angenommen. In verteilten Systemen können Fehler auf verschiedenen Ebenen auftreten – von Netzwerk-Timeouts über API-Limits bis hin zu Datenbank-Deadlocks. Eine robuste Fehlerbehandlungsstrategie erfordert daher mehrere defensive Schichten, die jeweils unterschiedliche Fehlerklassen adressieren. Die Verwendung von SQL Server für Audit-Logs bietet dabei einen entscheidenden Vorteil: Alle Fehlerfälle werden in einer strukturierten, abfragbaren Form gespeichert, was Post-Mortem-Analysen und Trend-Erkennung erheblich erleichtert. Im Gegensatz zu Log-Dateien oder NoSQL-Stores ermöglicht SQL Server komplexe Aggregationen wie "Zeige mir alle Fehler der letzten Woche, gruppiert nach Fehlertyp und betroffener SKU-Range".

Multi-Layer Error Handling

Layer 1: Automatic Retry (Azure Functions built-in)
├── Queue messages automatically retried on exception
├── Exponential backoff
└── MaxDequeueCount: 5 → dann Dead-Letter

Layer 2: Application-Level Retry (Polly)
├── HTTP calls: Retry 3x mit exponential backoff
├── Transient errors (timeout, 5xx) → Retry
└── Permanent errors (4xx) → No retry, log & alert

Layer 3: Dead-Letter Processing
├── Manual investigation queue
├── Alert sent to Ops
└── Re-queue option nach Fix

Layer 4: SQL Server Audit Logging
├── Alle Import-Transaktionen protokolliert
├── Granulare Fehler-Details mit Stack-Traces
├── Reporting & Analytics über historische Fehlertrends
└── Compliance-konform für Audit-Anforderungen

Layer 5: Monitoring & Alerting
├── Application Insights tracks:
│   ├── Function execution times
│   ├── Failure rates
│   ├── Dependency call durations (inkl. SQL Server)
│   └── Custom metrics (products/hour)
├── SQL Server-basierte Custom Metrics:
│   ├── Import success rate per batch
│   ├── Average processing time trends
│   ├── SKU-level failure patterns
│   └── API rate-limit hit frequency
├── Alerts:
│   ├── Failure rate > 5%: Warning
│   ├── Failure rate > 10%: Critical
│   ├── Import duration > 2 hours: Warning
│   ├── Dead-letter queue > 100 messages: Critical
│   └── SQL Server connection failures: Immediate escalation

Real-World Incident Example

Incident: Magento API Outage (2023-11-28)

Timeline:
09:00: Import startet
09:15: Magento API returns 503 Service Unavailable
09:15-09:45: 1,500 products in dead-letter queue
09:45: Alert fired → Ops team notified
10:00: Ops confirms Magento-side issue
10:30: Magento recovered
10:35: Dead-letter products manually re-queued
11:00: Import completed (delayed 2 hours)

Lessons:
├── Circuit breaker needed: Nach 5 Failures zu Magento, pause 5 min
├── Better vendor-status-monitoring: Subscribe to Magento status page
└── Automated dead-letter re-queue: When Magento healthy, auto-retry

Kosten-Vergleich: Legacy vs. Serverless

LEGACY SYSTEM (Annual Cost):
├── VM (Standard_D4s_v3): €200/month × 12 = €2,400
├── Storage (VM Disk): €40/month × 12 = €480
├── Ops-Zeit (monitoring, patching): 5 hours/month × €100/hour × 12 = €6,000
├── Incident-Response: ~3 incidents/month × 4 hours × €100/hour × 12 = €14,400
└── Total: €23,280/year

SERVERLESS SYSTEM (Annual Cost):
├── Azure Functions (Consumption): €50/month × 12 = €600
├── Premium Plan (for warm instances): €150/month × 12 = €1,800
├── Storage (Blob, Queue): €20/month × 12 = €240
├── Azure SQL Database (Basic Tier): €40/month × 12 = €480
├── Application Insights: €30/month × 12 = €360
├── Service Bus: €10/month × 12 = €120
├── Ops-Zeit: 1 hour/month × €100/hour × 12 = €1,200
└── Total: €4,800/year

Savings: €18,480/year (79% Reduktion)

Hinweis: Die Verwendung von Azure SQL Database statt Table Storage erhöht die Kosten um €240/Jahr,
bietet aber signifikante Vorteile:
├── Transaktionale Konsistenz (ACID-Garantien)
├── Komplexe Reporting-Queries (JOIN, Aggregationen)
├── Business Intelligence & Analytics-Integration
├── Etablierte Backup/Recovery-Mechanismen
└── Compliance-konforme Audit-Trails

ROI Factors:
├── Initial Development: €60,000 (6 months, 2 engineers)
├── Payback Period: 3.3 Jahre
├── Intangible benefits (scalability, reliability, flexibility) überwiegen bei weitem
└── SQL Server-Reporting spart geschätzt 10 Stunden/Monat für BI-Team (€12,000/Jahr)

Key Takeaways & Best Practices

1. Event-Driven ist der Schlüssel

  • Queue-based communication entkoppelt Services
  • Ermöglicht unabhängige Skalierung jeder Komponente
  • Retry und Dead-Letter automatisch

2. Serverless hat Trade-offs

  • Pro: Auto-scaling, pay-per-use, zero server management
  • Con: Cold starts, execution time limits, debugging schwieriger
  • Premium Plan für production-critical workloads (warm instances)

3. Legacy-Integration erfordert Resilience

  • External systems (ERP, Magento) sind außerhalb deiner Kontrolle
  • Rate-Limiting, Circuit-Breakers, Retries sind unverzichtbar
  • Dead-Letter-Queues für Poison-Messages

4. Monitoring ist nicht optional

  • Application Insights custom metrics for business-KPIs
  • Alerts für Failure-Rate, Duration-Anomalies
  • Dashboard für Stakeholder-Visibility

5. Batch-Operations wo möglich

  • Single API-calls skalieren nicht
  • Bulk-APIs (Magento: 100 products/call) dramatisch schneller
  • Trade-off: Komplexität vs. Performance

6. Kosten-Optimization ist kontinuierlich

  • Consumption vs. Premium: Messung nötig
  • Storage-Tier-Optimization (Hot vs. Cool)
  • Unused resources identifizieren und eliminieren

Fazit: Modern Integration Done Right

Die Transformation von monolithischer Windows-Service-Integration zu Event-Driven Serverless war herausfordernd, aber lohnenswert. Die Zahlen sprechen für sich:

  • Performance: 8 Stunden → 45 Minuten (89% schneller)
  • Reliability: 77% Erfolgsrate → 99.97% (99.7% Verbesserung)
  • Scalability: Fix-capacity → auto-scaling bis 10x Load
  • Cost: €23K/Jahr → €4K/Jahr (81% günstiger)
  • Ops-Overhead: 5 hours/Monat → 1 hour/Monat (80% Reduktion)

Die wichtigste Lektion: Serverless ist kein Silberbullet. Es erfordert architektonische Disziplin (event-driven thinking), Operational Excellence (monitoring, alerting), und continuous optimization. Aber für variable workloads mit klaren processing steps ist es eine überzeugende Alternative zu traditioneller Infrastruktur.

Für Organisationen, die ähnliche Legacy-Integration-Herausforderungen haben: Start small (ein Pilot-Workflow), measure everything (Kosten, Performance, Reliability), und iterate (continuous improvement). Die Cloud-Native-Reise ist Marathon, nicht Sprint – aber die Destination lohnt sich.

← Zurück zu allen Publikationen