System Integration #

Hampir tidak ada sistem modern yang berdiri sendiri. Backend service memanggil payment gateway, notification service mendengarkan event dari order service, mobile app mengonsumsi REST API, scheduler menarik data dari legacy ERP. Setiap titik hubung ini adalah integrasi — dan setiap integrasi yang tidak dirancang dengan benar adalah titik lemah yang bisa menjadi sumber data inconsistency, security breach, atau cascading failure. Tantangan integrasi bukan hanya teknis (“API bisa dipanggil”) melainkan desain (“bagaimana kalau payload berubah?”, “bagaimana kalau downstream down?”, “bagaimana kalau request yang sama datang dua kali?”). Artikel ini membahas system integration dari lima jenis utamanya, tiga pola komunikasi dengan trade-off masing-masing, best practice krusial mulai dari autentikasi hingga observability, implementasi konkret webhook dan HMAC verification, hingga studi kasus end-to-end lengkap.

Lima Jenis System Integration #

flowchart TD
    SI[System Integration] --> P2P["Point-to-Point\nKoneksi langsung"]
    SI --> API["API-Based\nKontrak REST/gRPC/GraphQL"]
    SI --> ED["Event-Driven\nMessage broker"]
    SI --> FB["File-Based\nBatch CSV/XML/Parquet"]
    SI --> DB["Database-Level\n❌ Anti-pattern"]

1. Point-to-Point Integration #

Setiap sistem terhubung langsung ke sistem lain tanpa mediator.

flowchart LR
    A[Sistem A] --> B[Sistem B]
    A --> C[Sistem C]
    D[Sistem D] --> B

Di awal terasa simple, tapi seiring bertambahnya sistem, jumlah koneksi tumbuh secara kuadratik: N sistem menghasilkan hingga N(N-1)/2 koneksi.

Jumlah SistemMaksimum Koneksi
510
1045
20190
501.225

Setiap koneksi adalah dependency yang harus dikelola, dimonitor, dan diuji. Kapan masih acceptable: prototipe, integrasi sementara antara dua sistem kecil, atau ketika kompleksitas mediator tidak sebanding.

2. API-Based Integration #

Sistem berkomunikasi melalui kontrak API yang terdefinisi — REST, gRPC, atau GraphQL.

flowchart LR
    M[Mobile App] -- "REST" --> B[Backend API]
    B -- "gRPC" --> I[Internal Service]
    B -- "REST" --> P[Payment Gateway]

Ini adalah pola yang paling umum dan paling fleksibel. Kunci yang sering dilewatkan: API bukan sekadar endpoint, ia adalah kontrak. Schema request/response yang berubah tanpa versioning adalah sumber breaking change yang merusak semua consumer sekaligus.

3. Event-Driven Integration #

Sistem berkomunikasi melalui event asynchronous melalui message broker — Kafka, RabbitMQ, AWS SNS/SQS, atau Google Pub/Sub.

flowchart TD
    OS[Order Service] -- "publish" --> K["(Kafka: order-events)"]
    K --> PS["Payment Service\nsubscribe"]
    K --> IS["Inventory Service\nsubscribe"]
    K --> AS["Analytics Service\nsubscribe"]

Keunggulan: loose coupling yang sesungguhnya — Order Service tidak tahu siapa yang mendengarkan eventnya. Menambahkan consumer baru tidak memerlukan perubahan pada producer. Kelemahannya: debugging lebih kompleks, dan sistem beroperasi dengan eventual consistency.

4. File-Based Integration #

Pertukaran data melalui file (CSV, XML, JSON, Parquet) yang ditempatkan di lokasi yang disepakati.

flowchart LR
    LE[Legacy ERP] -- "export batch\nfile.csv" --> SF[("(Shared Storage\nS3/SFTP)")]
    SF -- "scheduled\nimport job" --> NS[New System]

Paling umum di integrasi dengan sistem legacy (ERP, mainframe) yang tidak mendukung API modern, atau untuk batch job volume besar. Risikonya: delay tinggi (data baru tersedia setelah batch selesai), error handling sulit (file rusak di tengah proses), dan tidak ada konfirmasi real-time.

5. Database-Level Integration — Anti-Pattern #

Satu sistem mengakses database sistem lain secara langsung.

// ANTI-PATTERN: Payment Service langsung query DB milik Order Service
func (p *PaymentService) getOrderTotal(orderID string) float64 {
    row := orderDB.QueryRow(  // ← akses database milik sistem lain!
        "SELECT total FROM orders WHERE id = ?", orderID)
    // ...
}
flowchart LR
    subgraph Bad["❌ Database-Level Integration"]
        PS1[Payment Service] -->|"direct SQL query"| ODB[("(Order DB\nschema internal)")]
        OS1[Order Service] --> ODB
    end
    subgraph Good["✅ API/Event sebagai Antarmuka"]
        PS2[Payment Service] -->|"GET /orders/:id"| OAPI[Order Service API]
        OAPI --> ODB2[("(Order DB\nencapsulated)")]
    end

Ini melanggar encapsulation sepenuhnya: schema database menjadi public API yang tidak bisa diubah tanpa merusak konsumen. Payment Service sekarang bergantung pada internal implementation detail Order Service. Tidak ada cara untuk evolve schema database tanpa koordinasi dengan semua pihak yang mengaksesnya. Selalu gunakan API atau event sebagai antarmuka.


Tiga Pola Komunikasi Integrasi #

Synchronous Request-Response #

Client menunggu response sebelum melanjutkan. Cocok ketika hasil langsung dibutuhkan.

sequenceDiagram
    participant C as Client
    participant V as Payment Validator
    participant G as Payment Gateway

    C->>V: POST /validate
    V-->>C: {valid: true}
    C->>G: POST /charge
    G-->>C: {txn_id: "abc"}

Kapan pakai sync: validasi data yang menentukan apakah flow bisa dilanjutkan, query yang hasilnya langsung ditampilkan ke user, operasi dengan SLA latency ketat.

Risikonya: cascading failure — jika downstream lambat atau down, caller ikut terhambat atau gagal. Harus selalu dilengkapi timeout dan circuit breaker.

Asynchronous Messaging #

Producer mengirim message ke broker dan langsung selesai. Consumer memproses di waktu yang berbeda.

sequenceDiagram
    participant O as Order Service
    participant Q as Queue: send-confirmation-email
    participant E as Email Service

    O->>Q: publish
    O-->>O: return 202 (langsung selesai)
    Q->>E: consume (proses background)

Kapan pakai async: operasi yang tidak perlu hasilnya langsung (kirim email, generate report, sync ke CRM), proses yang membutuhkan waktu lama, operasi yang boleh eventual consistency.

Orchestration vs Choreography #

Dua pendekatan berbeda untuk mengkoordinasikan banyak service dalam sebuah workflow.

Orchestration — ada central coordinator yang mengarahkan workflow:

flowchart TD
    OC[Order Orchestrator] -->|"1. Validate inventory"| INV[Inventory Service]
    OC -->|"2. Process payment"| PAY[Payment Service]
    OC -->|"3. Create shipment"| SHIP[Shipping Service]
    OC -->|"4. Send notification"| NOTIF[Notification Service]

Choreography — setiap service bereaksi terhadap event secara mandiri:

flowchart TD
    OS[Order Service] -->|publish| EV[OrderCreated]
    EV --> PS["Payment Service\nproses, publish PaymentDone"]
    EV --> IS["Inventory Service\nkurangi stok, publish StockReduced"]
    EV --> SS["Shipping Service\nbuat label setelah PaymentDone"]
AspekOrchestrationChoreography
KontrolTerpusat di satu coordinatorTerdistribusi, setiap service mandiri
Visibilitas flowJelas, satu titik monitorSulit dilacak, butuh distributed tracing
Single point of failureYa — coordinatorTidak
CouplingService coupled ke coordinatorService hanya coupled ke event schema
Cocok untukWorkflow kompleks dengan banyak langkah bersyaratSistem yang butuh scaling dan independensi tinggi

Best Practice Krusial #

Authentication — Jangan Percaya Request dari Sistem Lain #

Setiap integrasi harus diautentikasi, termasuk internal service-to-service. “Internal network” bukan jaminan keamanan.

// Machine-to-machine auth dengan OAuth 2.0 Client Credentials
type ServiceClient struct {
    tokenURL     string
    clientID     string
    clientSecret string
    httpClient   *http.Client
    tokenCache   *TokenCache
}

func (c *ServiceClient) GetAccessToken(ctx context.Context) (string, error) {
    // Cek cache dulu — token valid selama 5 menit sebelum expired
    if token, ok := c.tokenCache.Get(); ok {
        return token, nil
    }

    resp, err := c.httpClient.PostForm(c.tokenURL, url.Values{
        "grant_type":    {"client_credentials"},
        "client_id":     {c.clientID},
        "client_secret": {c.clientSecret},
        "scope":         {"internal:read internal:write"},
    })
    // parse token dan cache dengan TTL = expires_in - 5 menit buffer
    token := parseToken(resp)
    c.tokenCache.Set(token, token.ExpiresIn-5*time.Minute)
    return token.AccessToken, nil
}

Pilihan autentikasi berdasarkan konteks:

KonteksMekanisme yang Direkomendasikan
Internal service (low trust)OAuth 2.0 Client Credentials + short-lived JWT
Internal service (high trust)mTLS (mutual TLS) + JWT
External webhookHMAC signature + timestamp
External third-party APIAPI Key + TLS
Mobile/web → backendOAuth 2.0 Authorization Code + PKCE

HMAC Webhook Signature Verification #

Webhook dari payment gateway, GitHub, Stripe, dan layanan serupa biasanya mengirim HMAC signature untuk membuktikan request berasal dari mereka dan payload tidak dimodifikasi.

sequenceDiagram
    participant PG as Payment Gateway
    participant W as Webhook Handler

    PG->>W: POST /webhook\nX-Signature: sha256=...\nX-Timestamp: ...
    W->>W: 1. Baca raw body
    W->>W: 2. Hitung HMAC dari raw body
    W->>W: 3. Bandingkan dengan X-Signature\n(constant-time)
    W->>W: 4. Cek timestamp freshness\n(< 5 menit)
    alt Valid
        W-->>PG: 200 OK (segera)
        W->>W: Proses async di background
    else Invalid
        W-->>PG: 401 Unauthorized
    end
// Webhook handler dengan HMAC signature verification
func (h *PaymentWebhookHandler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
    // Baca raw body SEBELUM parsing — signature dihitung dari raw bytes
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read body", http.StatusBadRequest)
        return
    }

    // Verifikasi signature
    if !h.verifySignature(body, r.Header.Get("X-Webhook-Signature")) {
        log.Warnf("invalid webhook signature from %s", r.RemoteAddr)
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    // Cek timestamp untuk mencegah replay attack
    timestamp := r.Header.Get("X-Webhook-Timestamp")
    if !h.isTimestampFresh(timestamp, 5*time.Minute) {
        log.Warnf("stale webhook timestamp: %s", timestamp)
        http.Error(w, "Request expired", http.StatusUnauthorized)
        return
    }

    // Return 200 SEGERA — proses async di background
    // Webhook provider biasanya timeout dalam 5-30 detik
    w.WriteHeader(http.StatusOK)

    // Proses di goroutine terpisah agar tidak timeout
    go h.processWebhookAsync(body)
}

func (h *PaymentWebhookHandler) verifySignature(body []byte, signature string) bool {
    mac := hmac.New(sha256.New, []byte(h.webhookSecret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    // Gunakan hmac.Equal untuk constant-time comparison
    // — mencegah timing attack
    return hmac.Equal([]byte(signature), []byte(expected))
}
Selalu gunakan hmac.Equal (constant-time comparison), bukan == atau bytes.Equal biasa, untuk membandingkan signature. Perbandingan biasa bisa membocorkan informasi melalui timing attack — penyerang bisa menebak signature byte demi byte dengan mengukur waktu respons.

Data Contract dan Versioning #

Schema adalah kontrak. Perubahan yang tidak backward-compatible merusak semua consumer tanpa pemberitahuan.

// BENAR: versioned API response — tambah field, jangan hapus
type OrderResponseV1 struct {
    OrderID   string    `json:"order_id"`
    Status    string    `json:"status"`
    Total     float64   `json:"total"`
    CreatedAt time.Time `json:"created_at"`
}

// V2: tambah field baru sebagai optional — consumer V1 tidak rusak
type OrderResponseV2 struct {
    OrderID        string    `json:"order_id"`
    Status         string    `json:"status"`
    Total          float64   `json:"total"`
    CreatedAt      time.Time `json:"created_at"`
    // Field baru — omitempty agar tidak breaking untuk consumer lama
    TrackingNumber *string   `json:"tracking_number,omitempty"`
    PromotionCode  *string   `json:"promotion_code,omitempty"`
}

Strategi versioning API:

StrategiContohKelebihanKekurangan
URL versioning/v1/orders, /v2/ordersPaling eksplisit, mudah di-route, consumer bisa migrate bertahapURL berubah antar versi
Header versioningAccept: application/vnd.api.v2+jsonURL bersihKurang discoverable
Query param/orders?version=2SimpleTidak standar

Idempotency Key untuk Operasi Kritis #

Setiap operasi yang tidak idempotent secara alami (create, charge) harus dibuat idempotent dengan idempotency key.

// Client menyertakan idempotency key di setiap request kritis
func (c *PaymentClient) CreateCharge(ctx context.Context, req ChargeRequest) (*Charge, error) {
    // Generate idempotency key berdasarkan intent — bukan random
    // Key yang sama untuk request yang sama memastikan deduplication bekerja
    idempotencyKey := fmt.Sprintf("charge:%s:%s:%d",
        req.OrderID,
        req.UserID,
        req.AmountCents,
    )

    httpReq, _ := http.NewRequestWithContext(ctx, "POST",
        c.baseURL+"/charges",
        encodeJSON(req),
    )
    httpReq.Header.Set("Idempotency-Key", idempotencyKey)
    httpReq.Header.Set("X-Correlation-ID", getCorrelationID(ctx))

    // Server akan deduplicate: jika key sudah ada, return hasil lama
    return c.do(httpReq)
}

Observability — Correlation ID di Seluruh Sistem #

Tanpa distributed tracing, debugging request yang melewati 5 service adalah mimpi buruk.

// Middleware yang propagasi correlation ID ke semua outbound request
type CorrelationMiddleware struct {
    next http.RoundTripper
}

func (m *CorrelationMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
    // Ambil dari context, atau generate jika tidak ada
    correlationID := getOrGenerateCorrelationID(req.Context())

    // Propagate ke downstream
    req.Header.Set("X-Correlation-ID", correlationID)
    req.Header.Set("X-Request-ID", uuid.New().String())

    return m.next.RoundTrip(req)
}

// Semua log harus menyertakan correlation ID
func logRequest(ctx context.Context, msg string, fields ...interface{}) {
    cid := ctx.Value("correlation_id")
    log.WithField("correlation_id", cid).Info(append([]interface{}{msg}, fields...)...)
}

Studi Kasus: Order → Payment → Notification #

Berikut flow end-to-end yang menerapkan semua best practice yang sudah dibahas.

sequenceDiagram
    participant U as User
    participant OS as Order Service
    participant K as Kafka
    participant PS as Payment Service
    participant IS as Inventory Service
    participant PG as Payment Gateway
    participant NS as Notification Service

    U->>OS: POST /checkout
    OS->>OS: 1. Create order (PENDING)
    OS->>K: 2. Publish OrderCreated
    OS-->>U: 3. {order_id, status: "pending"}

    K->>PS: consume OrderCreated
    K->>IS: consume OrderCreated
    IS->>IS: reserve stock

    PS->>PG: 4. Call payment gateway\n(with idempotency key)
    PG->>PS: webhook callback
    PS->>PS: 5. Verify HMAC signature
    PS->>PS: 6. Check idempotency
    PS->>PS: 7. Update order to PAID
    PS->>K: 8. Publish PaymentCompleted

    K->>NS: consume PaymentCompleted
    NS->>NS: send email + push notification
// Payment Service — handler untuk webhook dari gateway
func (s *PaymentService) HandleGatewayWebhook(ctx context.Context,
    payload WebhookPayload, signature string) error {

    // Verifikasi signature
    if !verifyHMAC(payload, signature, s.webhookSecret) {
        return ErrInvalidSignature
    }

    // Idempotency check — webhook bisa dikirim ulang
    if processed, _ := s.processedWebhooks.Exists(ctx, payload.WebhookID); processed {
        log.Infof("webhook %s already processed, skipping", payload.WebhookID)
        return nil
    }

    // Proses dalam satu transaction
    return s.db.Transaction(func(tx *gorm.DB) error {
        // Update order status
        if err := s.orderRepo.UpdateStatus(ctx, tx,
            payload.OrderID, "PAID"); err != nil {
            return err
        }

        // Publish event
        if err := s.eventBus.Publish(ctx, PaymentCompletedEvent{
            OrderID:       payload.OrderID,
            TransactionID: payload.TransactionID,
            Amount:        payload.Amount,
            PaidAt:        time.Now(),
            CorrelationID: getCorrelationID(ctx),
        }); err != nil {
            return err
        }

        // Tandai webhook sebagai processed
        return s.processedWebhooks.Set(ctx, tx, payload.WebhookID)
    })
}

Anti-Pattern yang Harus Dihindari #

// ✗ Trust internal request tanpa autentikasi
func handleInternalRequest(r *http.Request) {
    // "Ini dari internal, pasti aman"
    processRequest(r) // tidak ada auth check
}
// ✓ Verifikasi JWT atau mTLS bahkan untuk internal call

// ✗ Tidak ada timeout pada outbound call
resp, err := http.Get("https://external-api.com/data")
// Jika external API hang, goroutine ini hang selamanya
// ✓ Selalu set timeout
client := &http.Client{Timeout: 10 * time.Second}

// ✗ Menelan error dari downstream tanpa logging
result, err := externalService.Call(req)
if err != nil {
    return nil // error hilang tanpa jejak
}
// ✓ Log dengan context, propagate atau wrap dengan informasi yang cukup

// ✗ Hardcoded credentials
const apiKey = "sk_live_abc123" // ← di Git, terekspos ke semua developer
// ✓ Baca dari secret manager atau environment variable yang aman

// ✗ Webhook langsung diproses synchronously tanpa queue
func handleWebhook(w http.ResponseWriter, r *http.Request) {
    processHeavyOperation() // bisa timeout sebelum return 200
    w.WriteHeader(200)
}
// ✓ Return 200 segera, proses async di background

Checklist Integrasi Production-Ready #

KEAMANAN:
  □ Semua integrasi diautentikasi (tidak ada "trusted by default")
  □ Token/secret disimpan di secret manager (bukan hardcoded)
  □ Short-lived token dengan rotation
  □ Webhook signature diverifikasi dengan HMAC
  □ Timestamp freshness check untuk mencegah replay attack
  □ TLS untuk semua komunikasi

DATA CONTRACT:
  □ API schema terdokumentasi (OpenAPI/Protobuf)
  □ Versioning strategy didefinisikan
  □ Perubahan backward-compatible (tambah field, jangan hapus)
  □ Breaking change ditangani dengan major version baru

RELIABILITY:
  □ Timeout dikonfigurasi untuk semua outbound call
  □ Retry dengan exponential backoff + jitter
  □ Circuit breaker untuk downstream yang sering flaky
  □ Idempotency key untuk operasi yang tidak idempotent secara alami
  □ DLQ untuk event yang gagal diproses

OBSERVABILITY:
  □ Correlation ID di-generate di entry point dan dipropagasi ke semua downstream
  □ Setiap request/response di-log dengan correlation ID
  □ Metrics: error rate, latency, throughput per integration point
  □ Alert untuk error rate yang melebihi threshold

Ringkasan #

  • System integration menghubungkan sistem agar bisa bertukar data dan berkolaborasi — bukan sekadar “API saling panggil”, melainkan menyangkut kontrak, keamanan, dan reliability jangka panjang.
  • Lima jenis: point-to-point (simple tapi tidak scalable), API-based (standar dan fleksibel), event-driven (loose coupling tapi eventual consistency), file-based (untuk legacy/batch), dan database-level (anti-pattern — hindari).
  • Orchestration memberikan visibilitas flow yang jelas dengan central coordinator; choreography memberikan independence lebih tapi flow lebih sulit dilacak — pilih berdasarkan kebutuhan.
  • Autentikasi wajib bahkan untuk internal service — gunakan OAuth 2.0 Client Credentials atau mTLS, bukan “trusted by default”.
  • HMAC signature verification untuk webhook — termasuk constant-time comparison dan freshness check untuk mencegah timing attack dan replay attack.
  • Idempotency key wajib untuk operasi yang tidak secara alami idempotent — payment, order creation, aksi yang punya efek finansial.
  • Return 200 segera dari webhook endpoint, proses async di background — webhook provider biasanya timeout dalam 5–30 detik.
  • API adalah kontrak — perubahan schema harus backward-compatible; breaking change butuh major version baru.
  • Correlation ID harus di-generate di entry point dan dipropagasi ke semua downstream — tanpa ini, debugging request yang melewati banyak service sangat sulit.

← Sebelumnya: Jitter   Berikutnya: SOLID →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact