Single Source of Truth #
Bayangkan sebuah sistem di mana status user bernilai "ACTIVE" di database, "ENABLED" di backend B, dan "active" di frontend. Ketiganya bermaksud hal yang sama, tapi karena tidak ada satu sumber kebenaran yang otoritatif, setiap engineer harus menulis mapping yang berbeda, setiap bug report tentang status user butuh investigasi di tiga tempat, dan setiap perubahan definisi status berpotensi melewatkan salah satu representasi. Inilah masalah yang SSOT — Single Source of Truth — selesaikan: setiap data, aturan bisnis, atau konfigurasi yang penting harus memiliki satu representasi yang jelas dan otoritatif, dan semua bagian sistem harus mengacu ke representasi tunggal itu. Panduan ini membahas SSOT dari masalah nyata yang ia selesaikan, empat area penerapan yang paling umum, implementasi konkret di Go dan Dart, SSOT dalam konteks microservices, perbedaannya dengan DRY, hingga kapan penerapannya tidak sebanding dengan kompleksitas yang ditimbulkan.
Masalah Nyata Tanpa SSOT #
Ada tiga kategori masalah yang paling sering timbul ketika sistem tidak punya single source of truth.
Data tidak konsisten. Ini yang paling langsung terasa pengguna. Ketika definisi “user aktif” berbeda di setiap komponen, behavior sistem menjadi tidak terduga:
Tanpa SSOT: tiga representasi berbeda untuk satu konsep
Service A (backend):
const StatusActive = "ACTIVE"
if user.status == "ACTIVE" { allowAccess() }
Service B (worker):
if user.status == "ENABLED" { processJob() } // "ENABLED" bukan "ACTIVE"?
Frontend:
if (user.status === 'active') { showDashboard() } // lowercase?
Database:
status ENUM('active', 'inactive') // lowercase lagi
→ User dengan status "ACTIVE" di database:
Service A: dapat akses ✓
Service B: tidak dapat proses job ✗ (bug tersembunyi)
Frontend: tidak tampil dashboard ✗ (bug lain)
Duplikasi business rule. Aturan bisnis yang sama ditulis ulang di banyak tempat, masing-masing dengan sedikit variasi yang tidak disengaja. Ketika aturan berubah, satu tempat di-update dan yang lain terlupakan.
Biaya maintenance tinggi. Setiap perubahan pada data atau aturan yang tersebar memerlukan update di banyak tempat — dan selalu ada risiko satu tempat terlewat, yang menjadi sumber bug yang sulit dilacak karena tidak langsung terlihat.
SSOT Bukan Hanya Tentang Database #
Kesalahpahaman yang umum: SSOT berarti semua data harus ada di satu database. Ini salah. SSOT adalah tentang siapa atau apa yang menjadi sumber kebenaran otoritatif untuk suatu informasi — lokasinya bisa bermacam-macam.
SSOT bisa berupa:
Kode → package domain yang mendefinisikan tipe dan konstanta
Database → tabel yang menjadi sumber data canonical
Service → microservice yang menjadi owner suatu domain
Config file → satu file yang dibaca semua komponen
Schema → OpenAPI atau Protobuf yang mendefinisikan kontrak API
Event log → Kafka topic yang menjadi source of truth dalam event sourcing
Yang terpenting: authoritynya jelas, bukan lokasinya.
Jika ada pertanyaan "mana yang benar?", jawabannya harus satu.
Empat Area Penerapan SSOT #
1. Domain Rules dan Typed Enum #
Ini adalah area yang paling sering diabaikan dan paling sering menimbulkan masalah. Definisi status, tipe, dan konstanta bisnis harus ada di satu tempat sebagai domain knowledge yang otoritatif.
// ANTI-PATTERN: status user tersebar sebagai string literal
// service_a/handler.go
if user.Status == "ACTIVE" { /* ... */ }
// service_b/worker.go
if user.Status == "active" { /* case berbeda */ }
// service_c/report.go
if user.Status == "ENABLED" { /* nama berbeda */ }
// BENAR: typed enum di domain package — satu definisi untuk semua
// domain/user/status.go
package user
type Status string
const (
StatusActive Status = "ACTIVE"
StatusInactive Status = "INACTIVE"
StatusSuspended Status = "SUSPENDED"
)
// Method behavior juga ada di sini — bukan tersebar di setiap tempat yang pakai
func (s Status) IsActive() bool {
return s == StatusActive
}
func (s Status) CanLogin() bool {
return s == StatusActive // hanya active yang bisa login
}
func (s Status) IsValid() bool {
switch s {
case StatusActive, StatusInactive, StatusSuspended:
return true
}
return false
}
// Penggunaan di seluruh codebase — konsisten karena mengacu ke satu sumber
// handler.go
if user.Status.CanLogin() {
allowAccess(user)
}
// worker.go
if user.Status.IsActive() {
processUserJob(user)
}
// report.go
activeUsers := filter(users, func(u User) bool {
return u.Status.IsActive()
})
Dengan typed enum, compiler Go akan menangkap jika ada perbandingan dengan string yang tidak valid. Dan perubahan definisi “siapa yang bisa login” cukup dilakukan di satu method CanLogin().
2. Business Rules yang Terpusat #
Perhitungan, validasi, dan aturan bisnis harus ada di domain layer, bukan tersebar di handler, worker, dan service.
// ANTI-PATTERN: tax calculation tersebar
// checkout_handler.go
total := price + price*0.11
// invoice_service.go
finalPrice := basePrice * 1.11
// receipt_generator.go
amount := cost + cost*0.11
// Jika tax rate berubah ke 12%, harus update tiga tempat
// Dan ketiganya mungkin sudah ada di banyak file berbeda
// BENAR: SSOT untuk tax calculation
// domain/pricing/tax.go
package pricing
const TaxRate = 0.11 // satu-satunya definisi tax rate
type TaxCalculator struct{}
func (t TaxCalculator) AddTax(basePrice float64) float64 {
return basePrice * (1 + TaxRate)
}
func (t TaxCalculator) TaxAmount(basePrice float64) float64 {
return basePrice * TaxRate
}
// Perubahan tax rate: ubah satu konstanta, seluruh sistem otomatis mengikuti
3. Konfigurasi Terpusat #
Konfigurasi yang di-hardcode di banyak tempat adalah pelanggaran SSOT yang paling mudah terjadi.
// ANTI-PATTERN: config tersebar
// payment_service.go
client := &http.Client{Timeout: 30 * time.Second} // hardcoded
// notification_service.go
conn, _ := smtp.Dial("smtp.gmail.com:587") // hardcoded
// user_service.go
maxRetry := 3 // magic number
// BENAR: single config package
// config/config.go
package config
import (
"os"
"strconv"
"time"
)
type AppConfig struct {
Payment PaymentConfig
Notification NotificationConfig
Retry RetryConfig
}
type PaymentConfig struct {
GatewayURL string
Timeout time.Duration
APIKey string
}
type NotificationConfig struct {
SMTPHost string
SMTPPort int
From string
}
type RetryConfig struct {
MaxAttempts int
BaseDelay time.Duration
}
func Load() AppConfig {
return AppConfig{
Payment: PaymentConfig{
GatewayURL: os.Getenv("PAYMENT_GATEWAY_URL"),
Timeout: 30 * time.Second,
APIKey: os.Getenv("PAYMENT_API_KEY"),
},
Notification: NotificationConfig{
SMTPHost: os.Getenv("SMTP_HOST"),
SMTPPort: getEnvInt("SMTP_PORT", 587),
From: os.Getenv("EMAIL_FROM"),
},
Retry: RetryConfig{
MaxAttempts: getEnvInt("MAX_RETRY_ATTEMPTS", 3),
BaseDelay: 1 * time.Second,
},
}
}
func getEnvInt(key string, defaultVal int) int {
if val := os.Getenv(key); val != "" {
if n, err := strconv.Atoi(val); err == nil {
return n
}
}
return defaultVal
}
4. API Contract dan Schema #
Schema API adalah SSOT untuk komunikasi antar sistem. Schema yang tidak terdefinisi dengan jelas menjadi sumber salah paham antara frontend dan backend, atau antar microservice.
// Schema sebagai kontrak SSOT — menggunakan OpenAPI/Swagger
// Definisi satu kali, digunakan oleh semua consumer
// api/schema/order.go
package schema
// CreateOrderRequest adalah SSOT untuk format request create order
// Digunakan oleh HTTP handler, validation, dan dokumentasi
type CreateOrderRequest struct {
UserID string `json:"user_id" validate:"required,uuid"`
Items []Item `json:"items" validate:"required,min=1"`
Note string `json:"note,omitempty" validate:"max=500"`
}
type Item struct {
ProductID string `json:"product_id" validate:"required,uuid"`
Quantity int `json:"quantity" validate:"required,min=1,max=100"`
}
// CreateOrderResponse adalah SSOT untuk format response
type CreateOrderResponse struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
Total int64 `json:"total_cents"`
}
SSOT di Dart/Flutter #
Di Flutter, SSOT paling sering dilanggar dalam pengelolaan konstanta UI dan business rules yang tersebar di berbagai widget.
// ANTI-PATTERN: warna dan spacing tersebar di semua widget
class ProductCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Color(0xFF2196F3), // hardcoded blue
padding: EdgeInsets.all(16), // magic number
child: Text(
'In Stock',
style: TextStyle(color: Colors.green, fontSize: 12), // green hardcoded
),
);
}
}
class OrderSummary extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Color(0xFF2196F3), // sama, tapi siapa tahu?
padding: EdgeInsets.all(16),
child: Text(
'Available',
style: TextStyle(color: Colors.green, fontSize: 12),
),
);
}
}
// BENAR: design tokens sebagai SSOT untuk semua nilai visual
// lib/core/theme/app_colors.dart
class AppColors {
static const primary = Color(0xFF2196F3);
static const success = Color(0xFF4CAF50);
static const warning = Color(0xFFFFC107);
static const danger = Color(0xFFF44336);
}
// lib/core/theme/app_spacing.dart
class AppSpacing {
static const xs = 4.0;
static const sm = 8.0;
static const md = 16.0; // default padding
static const lg = 24.0;
static const xl = 32.0;
}
// lib/domain/product/product_status.dart
enum ProductStatus { inStock, outOfStock, limited }
extension ProductStatusUI on ProductStatus {
Color get color {
switch (this) {
case ProductStatus.inStock: return AppColors.success;
case ProductStatus.limited: return AppColors.warning;
case ProductStatus.outOfStock: return AppColors.danger;
}
}
String get label {
switch (this) {
case ProductStatus.inStock: return 'In Stock';
case ProductStatus.limited: return 'Limited';
case ProductStatus.outOfStock: return 'Out of Stock';
}
}
}
// Penggunaan — konsisten di semua widget
class ProductCard extends StatelessWidget {
final ProductStatus status;
const ProductCard({required this.status});
@override
Widget build(BuildContext context) {
return Container(
color: AppColors.primary,
padding: EdgeInsets.all(AppSpacing.md),
child: Text(
status.label,
style: TextStyle(color: status.color, fontSize: 12),
),
);
}
}
Perubahan warna “In Stock” cukup dilakukan di satu tempat — ProductStatusUI.color — dan semua widget yang menampilkan status produk otomatis mengikuti.
SSOT dalam Arsitektur Microservices #
Dalam microservices, SSOT bukan berarti satu database yang diakses semua service. Itu adalah anti-pattern yang disebut shared database — menghilangkan otonomi setiap service.
SSOT dalam microservices berarti data ownership yang jelas: setiap service adalah SSOT untuk domain yang menjadi tanggung jawabnya, dan service lain mengakses data tersebut melalui API atau event.
Salah — shared database anti-pattern:
User Service ──DB read──→ [Database orders]
Billing Service ──DB read──→ [Database orders]
Analytics Service ──DB read──→ [Database orders]
Semua service akses database Order Service langsung
→ perubahan schema orders merusak semua service sekaligus
→ tidak ada ownership yang jelas
Benar — data ownership per service:
Order Service = SSOT untuk order data
│
├─── Billing Service ──GET /api/orders/{id}──→ Order Service
│ (akses via API, bukan direct DB)
│
├─── Analytics Service ──subscribe──→ [order.events Kafka topic]
│ (akses via event, bukan direct DB)
│
└─── Report Service ──GET /api/orders?from=2026-01-01──→ Order Service
Jika Order Service mengubah schema internal:
→ Billing Service dan Analytics tidak terpengaruh selama API contract sama
→ Order Service bisa evolve internal implementation-nya secara independen
Data yang di-copy ke service lain bukan pelanggaran SSOT selama clear bahwa itu adalah derived data atau cache, bukan sumber kebenaran. Jika Billing Service menyimpan snapshot harga dari Order Service, itu bukan SSOT violation — selama semua tahu bahwa “kebenaran” harga ada di Order Service, dan Billing Service hanya menyimpan copy untuk efisiensi.
SSOT vs DRY — Mirip tapi Berbeda #
Keduanya tentang menghindari duplikasi, tapi fokusnya berbeda.
DRY (Don't Repeat Yourself):
Fokus: eliminasi duplikasi implementasi/kode
Level: implementasi
Pertanyaan: "Apakah logika ini sama di dua tempat?"
SSOT (Single Source of Truth):
Fokus: kebenaran data dan aturan bisnis
Level: arsitektur dan domain
Pertanyaan: "Siapa yang berhak mendefinisikan ini?"
Contoh perbedaan:
DRY: dua fungsi dengan loop yang sama → ekstrak ke helper
SSOT: dua service yang menyimpan "user status" → tentukan service mana yang SSOT
Keduanya saling melengkapi:
DRY mencegah duplikasi cara menulis sesuatu
SSOT memastikan satu tempat yang berhak mendefinisikan apa yang benar
Anti-Pattern SSOT yang Sering Ditemui #
// ✗ String literal yang sama tersebar
// file_a.go
if status == "active" { ... }
// file_b.go
if status == "ACTIVE" { ... } // berbeda case
// ✓ Typed constant di domain package
// ✗ Tax rate hardcoded di banyak tempat
// checkout.go: price * 1.11
// invoice.go: base * 1.11
// ✓ const TaxRate di domain/pricing
// ✗ Shared database — setiap service baca DB service lain langsung
// billing_service.go
db.Query("SELECT * FROM orders WHERE ...") // DB milik order-service!
// ✓ Akses via API atau event dari service yang bersangkutan
// ✗ Config environment variable berbeda nama untuk hal yang sama
// Satu service pakai DB_HOST, service lain pakai DATABASE_HOST
// ✓ Standarisasi nama env var di shared config documentation
// ✗ Enum yang sama didefinisikan ulang di frontend
// Backend Go: StatusActive = "ACTIVE"
// Frontend JS: const ACTIVE = 'active' // lowercase, berbeda!
// ✓ Generate frontend types dari backend schema (OpenAPI → TypeScript)
Kapan SSOT Bisa Menjadi Over-Engineering #
SSOT memiliki biaya tersendiri — terutama koordinasi antar tim dalam sistem terdistribusi. Ada situasi di mana overhead ini tidak sebanding.
SSOT sangat valuable ketika:
✓ Banyak bagian sistem mengacu pada informasi yang sama
✓ Perubahan pada informasi itu akan berdampak luas jika tidak terkoordinasi
✓ Ada risiko nyata dari inconsistency (finansial, keamanan, UX)
SSOT mungkin over-engineering ketika:
✗ Sistem sangat kecil dengan satu service dan satu consumer
✗ "Data" yang diduplikasi tidak akan pernah perlu sinkron
✗ Abstraksi untuk SSOT membuat alur lebih sulit dipahami
✗ Koordinasi untuk maintain SSOT lebih mahal dari inconsistency-nya
Ringkasan #
- SSOT berarti setiap data, aturan bisnis, dan konfigurasi yang penting memiliki satu representasi otoritatif — dan semua bagian sistem mengacu ke sana.
- Bukan hanya tentang database: SSOT bisa berupa package domain, service, schema file, atau event log — yang penting authoritynya jelas.
- Typed enum adalah cara paling elegan menerapkan SSOT untuk domain status dan konstanta — compiler membantu enforce, behavior bisa di-centralize.
- Business rules seperti tax calculation harus ada di domain layer, bukan tersebar di handler, worker, dan service terpisah.
- Config terpusat mencegah magic number tersebar; baca config sekali, sebarkan via dependency injection.
- Design tokens di Flutter (AppColors, AppSpacing, ProductStatus) adalah SSOT untuk nilai-nilai visual — perubahan di satu tempat otomatis berlaku di semua widget.
- Microservices: setiap service adalah SSOT untuk domain-nya, service lain akses via API atau event — bukan direct database access.
- SSOT ≠ DRY: DRY tentang duplikasi implementasi, SSOT tentang kebenaran data dan siapa yang berhak mendefinisikannya.
- Jangan over-engineer: SSOT paling valuable untuk data dan aturan yang banyak dikonsumsi — tidak perlu untuk setiap konstanta kecil yang hanya dipakai sekali.