Pondasi Backward Compatibility: Pelajaran dari Windows API

Windows API (WinAPI) mempertahankan kompatibilitas biner lintas dekade. Biner Windows 95 masih dapat dieksekusi di Windows 11 tanpa kompilasi ulang. Menjaga kompatibilitas jangka panjang menuntut pemisahan tegas antara kontrak publik dan implementasi internal. Ketika sebuah kontrak dipublikasikan, setiap modifikasi yang merusak kontrak tersebut membebankan biaya migrasi langsung ke konsumen API.

Stabilitas Kontrak: API vs ABI

Kompatibilitas terbagi menjadi dua level: Application Programming Interface (API) dan Application Binary Interface (ABI). Stabilitas API menjamin kode sumber tetap dapat dikompilasi tanpa modifikasi sintaks. Stabilitas ABI menjamin biner yang telah terkompilasi tetap dapat berjalan tanpa tautan ulang (relinking), mempertahankan calling convention, layout memori, dan penyelarasan struktur data (struct alignment).

Pada arsitektur web service atau microservices, stabilitas ABI ekuivalen dengan stabilitas protokol serialisasi data jaringan (misal: JSON payload, Protobuf tag IDs). Mengubah tipe data, membuang field, atau mengubah semantik status code melanggar kontrak runtime yang analog dengan pelanggaran ABI.

Pola Struct Versioning: Belajar dari cbSize

WinAPI memecahkan masalah evolusi payload menggunakan field ukuran di awal struktur, umumnya dinamai cbSize. Panggilan fungsi memeriksa ukuran byte struktur yang dikirim caller untuk menentukan versi layout memori yang digunakan konsumen, lalu menangani data sesuai versi tersebut tanpa menyebabkan buffer overrun.

Pola serupa dapat diterapkan pada perancangan API modern untuk menjaga interoperabilitas data model tanpa perlu membuat endpoint URL baru (/v1, /v2, /v3) untuk setiap penambahan atribut opsional.

Implementasi Struct Versioning di Go

Contoh berikut mendemonstrasikan deserialisasi payload yang kompatibel ke belakang menggunakan pengecekan kapabilitas schema berbasis ukuran dan field opsional:

package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

// UserPayloadV1 merepresentasikan skema data awal.
type UserPayloadV1 struct {
	Version int    `json:"version"`
	ID      string `json:"id"`
	Name    string `json:"name"`
}

// UserPayloadV2 menambahkan field tanpa merusak parser lama.
type UserPayloadV2 struct {
	UserPayloadV1
	Email string   `json:"email,omitempty"`
	Roles []string `json:"roles,omitempty"`
}

// IngestUser memproses payload secara adaptif berdasarkan field metadata version.
func IngestUser(data []byte) (*UserPayloadV2, error) {
	var meta struct {
		Version int `json:"version"`
	}
	if err := json.Unmarshal(data, &meta); err != nil {
		return nil, err
	}

	result := &UserPayloadV2{}

	switch meta.Version {
	case 1:
		var v1 UserPayloadV1
		if err := json.Unmarshal(data, &v1); err != nil {
			return nil, err
		}
		result.UserPayloadV1 = v1
		result.Roles = []string{"viewer"} // Default fallback untuk schema lama
	case 2:
		if err := json.Unmarshal(data, result); err != nil {
			return nil, err
		}
		if len(result.Roles) == 0 {
			result.Roles = []string{"viewer"}
		}
	default:
		return nil, errors.New("unsupported payload version")
	}

	if result.ID == "" || result.Name == "" {
		return nil, errors.New("missing required fields")
	}

	return result, nil
}

func main() {
	payloadV1 := []byte(`{"version": 1, "id": "usr_101", "name": "Budi"}`)
	payloadV2 := []byte(`{"version": 2, "id": "usr_102", "name": "Siti", "email": "[email protected]", "roles": ["admin"]}`)

	user1, _ := IngestUser(payloadV1)
	user2, _ := IngestUser(payloadV2)

	fmt.Printf("V1 -> ID: %s, Name: %s, Roles: %v\n", user1.ID, user1.Name, user1.Roles)
	fmt.Printf("V2 -> ID: %s, Email: %s, Roles: %v\n", user2.ID, user2.Email, user2.Roles)
}

Biaya Operasional: Layer Emulasi dan Technical Debt

Menjaga kompatibilitas penuh membawa konsekuensi technical debt. Windows menyelesaikan kompatibilitas perilaku rusak menggunakan Application Compatibility Shims (komponen seperti AcLayers.dll). Jika aplikasi lawas bergantung pada bug sistem operasi untuk berjalan normal, shim layer mendeteksi hash aplikasi tersebut lalu menyimulasikan bug spesifik tersebut secara sengaja.

Pada arsitektur sistem terdistribusi, layer emulasi ini terwujud sebagai adapter middleware atau API Gateway mapping logic. Trade-off yang muncul:

  • Overhead Pemeliharaan: Semakin banyak shim yang hidup di codebase, kompleksitas jalur pengujian (test paths) meningkat secara eksponensial.
  • Latensi Runtime: Setiap translation layer menambahkan alokasi memori tambahan dan evaluasi kondisi bersarang.
  • Code Rot: Fungsionalitas inti sulit direfaktor karena harus mempertahankan side-effects yang tidak sengaja bocor ke pengguna di masa lalu.

Strategi Deprecation Tanpa Breaking Changes

Depresiasi API harus mengikuti siklus terukur agar sistem konsumen memiliki waktu mitigasi tanpa insiden operasional:

  1. Add-Only Evolution: Tambahkan field opsional baru. Jangan pernah menghapus, mengubah tipe, atau memodifikasi semantik field yang ada.
  2. Sunset Headers: Berikan peringatan terstandarisasi melalui HTTP Header (misal: Deprecation: true dan Sunset: Wed, 11 Nov 2026 00:00:00 GMT) sesuai RFC 8594.
  3. Tombstoning: Jika field atau endpoint harus dipensiunkan, kunci nilai output menjadi nilai default statis alih-alih melempar exception 404 Not Found atau 500 Internal Error.
  4. Traffic Partitioning: Gunakan routing gateway untuk mengisolasi traffic legacy ke cluster adapter terpisah, mencegah degradasi performa pada pipeline modern.