Case Study: SiTepat — Customer-Facing App Enterprise Isla...
Engineering Case Study: Project SiTepat
"The best technology disappears — the customer only sees trust." — Don Norman
Context
Saya bertanggung jawab atas backend engineering aplikasi ini — merancang dan membangun API layer, Core Banking integration, notification engine, dan security middleware. Aplikasi ini adalah sisi customer-facing dari ekosistem Tepati — jika Tepati digunakan oleh petugas internal, SiTepat digunakan langsung oleh nasabah.
SiTepat adalah aplikasi mobile yang memberikan akses mandiri kepada nasabah (Undisclosed Enterprise Bank) untuk memantau status pembiayaan, tabungan, dan jadwal angsuran mereka — tanpa perlu mengunjungi kantor cabang atau menghubungi petugas.
Artikel ini membedah tantangan engineering dan keputusan arsitektur di balik pengembangan aplikasi customer-facing untuk sektor ultra-micro banking.
🏔️ 1. The Challenge
Kondisi Sebelum SiTepat
Sebelum SiTepat ada, nasabah hanya memiliki cara-cara tradisional untuk memantau status pembiayaan mereka:
- Buku angsuran fisik — dicatat manual oleh petugas lapangan, rawan salah tulis dan hilang.
- Menghubungi petugas (Community Officer) — nasabah harus menelpon atau menunggu kunjungan petugas untuk menanyakan saldo dan jadwal angsuran.
- Datang ke kantor cabang — memakan waktu dan biaya transportasi, terutama untuk nasabah di daerah terpencil.
- Tidak ada notifikasi — nasabah sering lupa tanggal jatuh tempo, menyebabkan keterlambatan pembayaran yang sebenarnya tidak disengaja.
Business Impact
Sebelum SiTepat, ~15% keterlambatan angsuran disebabkan oleh nasabah yang lupa tanggal jatuh tempo — bukan karena tidak mampu bayar. Setiap keterlambatan berpotensi meningkatkan rasio Non-Performing Loan (NPL) dan menurunkan credit score nasabah.
Tantangan Engineering
Membangun aplikasi untuk nasabah banking memiliki tantangan yang fundamental berbeda dari aplikasi internal (Tepati):
- Security Surface Area: Aplikasi terekspos ke publik melalui Play Store — attack surface jauh lebih besar dibanding app internal yang terdistribusi via MDM.
- User Diversity: Target pengguna adalah nasabah mikro — pedagang pasar, petani, penjahit — yang mungkin baru pertama kali menggunakan smartphone untuk layanan perbankan.
- Real-time Accuracy: Nasabah berekspektasi saldo terupdate seketika setelah pembayaran. Delay 5 detik saja bisa memicu pertanyaan ke Call Center.
- Legacy Integration: Core Banking System bank menggunakan protokol ISO 8583 (standar transaksi ATM/POS dari era 1987) — harus di-bridge ke REST API modern.
- Scale Pattern: Traffic bersifat bursty — lonjakan masif terjadi di tanggal-tanggal jatuh tempo (tanggal 1, 5, 10, 15 setiap bulan).
- Device Fragmentation: Nasabah menggunakan Android devices dengan spesifikasi sangat bervariasi — dari RAM 1GB hingga flagship, Android 5.0 hingga terbaru.
🏗️ 2. Architecture Overview
Berbeda dengan Tepati yang arsitekturnya offline-first dengan message broker, SiTepat menggunakan arsitektur API Gateway pattern yang mengabstraksi kompleksitas Core Banking System dari mobile client.
Key Architecture Decisions:
- API Gateway sebagai single entry point — seluruh request dari mobile app masuk melalui satu gateway yang menangani auth, rate limiting, dan request routing.
- ISO 8583 Bridge Service — service khusus yang mentranslasi REST API calls ke format ISO 8583 yang dipahami Core Banking, dan sebaliknya.
- Redis Caching Layer — data yang jarang berubah (profil nasabah, detail tenor) di-cache untuk mengurangi load ke Core Banking System.
- Circuit Breaker — jika Core Banking System lambat atau down, circuit breaker aktif dan menampilkan data dari cache terakhir, bukan error.
🛠️ 3. Technology Stack
Backend (yang saya bangun)
- Golang: API Gateway dan microservices — dipilih karena low-latency dan built-in concurrency untuk handle bursty traffic.
- Microsoft SQL Server: Primary database untuk data transactional.
- MongoDB: Database NoSQL untuk menyimpan audit log aktivitas nasabah yang massive.
- Redis: Distributed caching — TTL-based untuk data saldo (30 detik), longer TTL untuk profil (5 menit).
- Firebase Cloud Messaging (FCM): Push notification engine untuk payment reminders.
- OAuth 2.0 + JWT: Authentication dan authorization framework.
Frontend (Mobile)
- React Native: Cross-platform app yang di-deploy ke Android via Play Store.
- Secure Storage: Encrypted keychain untuk menyimpan token dan MPIN hash.
- Firebase Analytics & Crashlytics: Monitoring adopsi dan stabilitas aplikasi.
Integration Layer
- ISO 8583 Adapter: Custom-built adapter untuk berkomunikasi dengan legacy Core Banking (Temenos/T24-based).
- Message Queue: Untuk async processing notification schedules dan audit logging.
🔍 4. Feature Deep Dives
Feature #1: Core Banking Integration via ISO 8583 Bridge
Problem: Core Banking System menggunakan protokol ISO 8583 — format binary fixed-length yang didesain tahun 1987 untuk transaksi ATM. Semua data saldo, mutasi, dan pembiayaan hanya bisa diakses melalui protokol ini. Mobile app butuh REST API.
Solution: Saya membangun ISO 8583 Bridge Service — sebuah Golang service yang bertindak sebagai translator antara REST API (JSON) dan ISO 8583 (binary).
// ISO 8583 Bridge - Simplified Balance Inquiry
type BalanceInquiry struct {
PAN string `iso8583:"2"` // Primary Account Number
ProcessCode string `iso8583:"3"` // 310000 = Balance Inquiry
Amount int64 `iso8583:"4"`
STAN string `iso8583:"11"` // System Trace Audit Number
LocalTime string `iso8583:"12"`
LocalDate string `iso8583:"13"`
MerchantID string `iso8583:"42"`
}
func (s *BridgeService) GetBalance(ctx context.Context, accountID string) (*BalanceResponse, error) {
// Build ISO 8583 message
msg := &BalanceInquiry{
PAN: accountID,
ProcessCode: "310000",
STAN: generateSTAN(),
LocalTime: time.Now().Format("150405"),
LocalDate: time.Now().Format("0102"),
}
packed, err := s.packer.Pack(msg)
if err != nil {
return nil, fmt.Errorf("failed to pack ISO message: %w", err)
}
// Send to Core Banking via TCP socket
resp, err := s.conn.SendWithTimeout(ctx, packed, 10*time.Second)
if err != nil {
// Circuit breaker: fallback to cached balance
cached, cacheErr := s.cache.Get(ctx, fmt.Sprintf("balance:%s", accountID))
if cacheErr == nil {
return cached.(*BalanceResponse), nil
}
return nil, fmt.Errorf("core banking unavailable: %w", err)
}
balance, err := s.packer.Unpack(resp)
if err != nil {
return nil, fmt.Errorf("failed to unpack response: %w", err)
}
// Cache the result
s.cache.Set(ctx, fmt.Sprintf("balance:%s", accountID), balance, 30*time.Second)
return balance, nil
}Key Engineering Decisions:
- TCP Connection Pool: Maintain persistent TCP connections ke Core Banking — avoid handshake overhead per request.
- Circuit Breaker Pattern: Jika Core Banking response time > 10 detik atau error rate > 50%, otomatis fallback ke cached data.
- STAN (System Trace Audit Number): Unique identifier per transaksi untuk reconciliation dan debugging.
Results:
| Metric | Sebelum (Manual) | Sesudah (SiTepat) | Improvement |
|---|---|---|---|
| Balance inquiry response time | N/A (visit branch) | < 2 detik | — |
| Core Banking hit rate | 100% (no cache) | 35% (65% cache hit) | ↓ 65% load |
| API availability | N/A | 99.7% (with circuit breaker) | — |
| Daily active inquiries | 0 (manual only) | ~120,000 | — |
Feature #2: Real-Time Financing Dashboard
Problem: Nasabah tidak memiliki visibility atas status pembiayaan mereka. Informasi seperti sisa plafon, jadwal angsuran, dan riwayat pembayaran hanya bisa dilihat di buku angsuran fisik — yang sering hilang atau tidak diupdate.
Solution: Financing Dashboard yang menampilkan data pembiayaan secara komprehensif dan real-time.
// Financing Dashboard - Data Structure (React Native)
interface FinancingDetail {
contractId: string;
productName: string;
disbursementDate: string;
maturityDate: string;
plafond: number;
outstandingBalance: number;
marginRate: number;
tenor: number;
installment: {
monthly: number;
nextDueDate: string;
remainingInstallments: number;
paidInstallments: number;
};
paymentHistory: PaymentRecord[];
status: "active" | "completed" | "overdue" | "restructured";
}
interface PaymentRecord {
date: string;
amount: number;
method: "auto_debit" | "cash_to_officer" | "transfer";
status: "paid" | "late" | "partial";
lateDays: number;
}
// Fetch with caching strategy
async function fetchFinancingDetail(
contractId: string,
): Promise<FinancingDetail> {
// Try local cache first (stale-while-revalidate pattern)
const cached = await SecureStorage.get(`financing:${contractId}`);
if (cached && !isStale(cached, 60_000)) {
return cached.data;
}
try {
const response = await api.get(`/financing/${contractId}`, {
headers: { Authorization: `Bearer ${await getToken()}` },
});
await SecureStorage.set(`financing:${contractId}`, {
data: response.data,
timestamp: Date.now(),
});
return response.data;
} catch (error) {
// Offline fallback: return cached data even if stale
if (cached) return cached.data;
throw new FinancingFetchError(error);
}
}Key Design:
- Stale-While-Revalidate: Tampilkan data dari cache dulu untuk instant UX, lalu update di background. Nasabah tidak perlu lihat loading spinner.
- Payment Progress Ring: Visual indicator berapa persen angsuran sudah selesai — memberi motivasi psychological kepada nasabah.
- Overdue Alert: Jika ada angsuran telat, card pembiayaan berubah merah dengan call-to-action yang jelas.
Results:
| Metric | Sebelum | Sesudah | Improvement |
|---|---|---|---|
| Customer inquiry calls (Call Center) | ~8,500/bulan | ~3,200/bulan | ↓ 62% |
| Avg time to check financing status | 15-30 menit (visit/call) | < 5 detik | ↓ 99% |
| Customer satisfaction (survey) | 3.2/5 | 4.5/5 | ↑ 41% |
| Buku angsuran replacement requests | ~2,100/bulan | ~400/bulan | ↓ 81% |
Feature #3: Smart Payment Reminder Engine
Problem: ~15% keterlambatan angsuran disebabkan karena lupa, bukan karena tidak mampu. Sistem SMS reminder sebelumnya tidak efektif karena sering masuk spam folder dan tidak bisa di-track delivery-nya.
Solution: Multi-channel smart reminder engine dengan scheduling yang configurable.
// Smart Reminder Scheduler (Golang)
type ReminderSchedule struct {
DaysBefore int
Template string
Priority string
Channel string
}
var reminderSchedules = []ReminderSchedule{
{DaysBefore: 3, Template: "reminder_gentle", Priority: "normal", Channel: "push"},
{DaysBefore: 1, Template: "reminder_urgent", Priority: "high", Channel: "push"},
{DaysBefore: 0, Template: "reminder_dueday", Priority: "high", Channel: "push"},
{DaysBefore: -1, Template: "reminder_overdue", Priority: "critical", Channel: "push+sms"},
}
func (s *ReminderService) ProcessDailyReminders(ctx context.Context) error {
today := time.Now().Truncate(24 * time.Hour)
for _, schedule := range reminderSchedules {
targetDate := today.AddDate(0, 0, schedule.DaysBefore)
customers, err := s.repo.FindByDueDate(ctx, targetDate)
if err != nil {
return fmt.Errorf("failed to fetch customers for %s: %w", targetDate, err)
}
for _, customer := range customers {
// Skip if already paid
if customer.IsPaidForCurrentPeriod() {
continue
}
notification := Notification{
CustomerID: customer.ID,
DeviceToken: customer.FCMToken,
Template: schedule.Template,
Priority: schedule.Priority,
Data: map[string]string{
"contract_id": customer.ContractID,
"amount": formatCurrency(customer.InstallmentAmount),
"due_date": targetDate.Format("2 January 2006"),
},
}
if err := s.notifier.Send(ctx, notification); err != nil {
s.logger.Error("failed to send reminder",
"customer_id", customer.ID,
"error", err,
)
// Don't fail entire batch for one customer
continue
}
s.repo.LogNotification(ctx, notification)
}
s.logger.Info("reminders processed",
"schedule", schedule.Template,
"target_date", targetDate,
"count", len(customers),
)
}
return nil
}Key Engineering:
- Batch Processing: Scheduler berjalan setiap pagi jam 06:00 WIB — memproses seluruh nasabah yang jatuh tempo di batch, bukan satu per satu.
- Idempotent: Jika scheduler berjalan dua kali (misalnya karena restart), nasabah tidak akan menerima notifikasi ganda — deduplication berdasarkan
customer_id + template + date. - Overdue Escalation: Jika nasabah sudah H+1 dan belum bayar, notifikasi juga dikirim via SMS sebagai fallback — bukan hanya push notification.
- Skip Logic: Nasabah yang sudah bayar di-skip otomatis, menghindari notifikasi yang mengganggu.
Results:
| Metric | Sebelum (SMS) | Sesudah (SiTepat) | Improvement |
|---|---|---|---|
| Late payment (karena lupa) | ~15% | ~4% | ↓ 73% |
| Notification delivery rate | 62% (SMS) | 94% (Push) | ↑ 52% |
| Nasabah bayar sebelum jatuh tempo | 58% | 81% | ↑ 40% |
| Cost per notification | Rp 350 (SMS) | Rp 0 (Push) | ↓ 100% |
Feature #4: Banking-Grade Security Layer
Problem: Aplikasi banking terekspos ke publik — setiap vulnerability bisa berujung pada pencurian data finansial nasabah. Regulasi OJK dan PBI (Peraturan Bank Indonesia) mensyaratkan standar keamanan yang sangat tinggi.
Solution: Multi-layered security framework yang saya implementasikan di backend dan koordinasikan dengan tim mobile.
// Security Middleware Stack (Golang)
func SecurityMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 1. Rate Limiting — per account, sliding window
accountID := extractAccountID(r)
if !rateLimiter.Allow(accountID, 30, time.Minute) {
http.Error(w, "too many requests", http.StatusTooManyRequests)
auditLog(ctx, "rate_limit_exceeded", accountID)
return
}
// 2. JWT Validation with rotation support
claims, err := validateJWT(r.Header.Get("Authorization"))
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
auditLog(ctx, "invalid_token", accountID)
return
}
// 3. Device Integrity Check
deviceFingerprint := r.Header.Get("X-Device-Fingerprint")
if isRootedDevice(deviceFingerprint) {
http.Error(w, "device not supported", http.StatusForbidden)
auditLog(ctx, "rooted_device_blocked", accountID)
alertSecurityTeam(ctx, accountID, "rooted_device_detected")
return
}
// 4. Session Binding — token harus dari device yang sama
if claims.DeviceID != deviceFingerprint {
http.Error(w, "session invalid", http.StatusUnauthorized)
auditLog(ctx, "device_mismatch", accountID)
return
}
// 5. Request Signing Verification
if !verifyRequestSignature(r) {
http.Error(w, "invalid signature", http.StatusBadRequest)
auditLog(ctx, "invalid_signature", accountID)
return
}
ctx = context.WithValue(ctx, "claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Security Layers Implemented:
| Layer | Mekanisme | Tujuan |
|---|---|---|
| Transport | TLS 1.3 + Certificate Pinning | Mencegah MITM attack |
| Authentication | MPIN + Biometric + OAuth 2.0 | Multi-factor verification |
| Authorization | JWT dengan device binding | Token hanya valid di device asli |
| Integrity | Root/Jailbreak detection | Block device yang compromised |
| Rate Limiting | Sliding window per account | Mencegah brute force dan DDoS |
| Audit | Immutable audit log | Compliance dan forensic |
| Data at Rest | AES-256 encryption | Lindungi data lokal di device |
| Request Signing | HMAC-SHA256 | Pastikan request tidak di-tamper |
Results:
| Metric | Target (OJK/PBI) | Achieved | Status |
|---|---|---|---|
| Security incidents (data breach) | 0 | 0 | ✅ Compliant |
| Unauthorized access attempts blocked | — | ~2,400/bulan | Auto-blocked |
| Brute force MPIN attempts blocked | — | ~850/bulan | Auto-locked |
| OJK penetration test | Pass | Pass | ✅ Certified |
Feature #5: Mutation History & Transaction Search
Problem: Nasabah sering memerlukan bukti transaksi untuk keperluan bisnis mereka — misalnya bukti bayar angsuran untuk mengajukan pinjaman di tempat lain, atau memastikan setoran dari petugas sudah masuk ke rekening.
Solution: Searchable transaction history dengan filter dan export capability.
// Transaction History Service (Golang)
type MutationFilter struct {
AccountID string `json:"account_id"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Type string `json:"type"` // "credit", "debit", "all"
MinAmount *int64 `json:"min_amount,omitempty"`
MaxAmount *int64 `json:"max_amount,omitempty"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func (s *MutationService) GetMutations(ctx context.Context, filter MutationFilter) (*MutationResponse, error) {
// Validate date range — max 90 days per query (OJK requirement)
if filter.EndDate.Sub(filter.StartDate) > 90*24*time.Hour {
return nil, fmt.Errorf("date range exceeds 90 days maximum")
}
// Check cache for recent mutations (< 24 hours)
cacheKey := fmt.Sprintf("mutations:%s:%s:%s",
filter.AccountID,
filter.StartDate.Format("20060102"),
filter.EndDate.Format("20060102"),
)
if cached, err := s.cache.Get(ctx, cacheKey); err == nil {
return cached.(*MutationResponse), nil
}
// Fetch from Core Banking via ISO 8583
rawMutations, err := s.bridge.FetchMutations(ctx, filter.AccountID, filter.StartDate, filter.EndDate)
if err != nil {
return nil, fmt.Errorf("failed to fetch mutations: %w", err)
}
// Transform & enrich data
mutations := make([]Mutation, 0, len(rawMutations))
for _, raw := range rawMutations {
mutations = append(mutations, Mutation{
Date: raw.TransactionDate,
Description: enrichDescription(raw.Description),
Amount: raw.Amount,
Type: categorizeTransaction(raw.DebitCredit),
Balance: raw.RunningBalance,
Reference: raw.ReferenceNumber,
})
}
// Apply client-side filters
filtered := applyFilters(mutations, filter)
// Paginate
result := paginate(filtered, filter.Page, filter.PageSize)
// Cache for 5 minutes
s.cache.Set(ctx, cacheKey, result, 5*time.Minute)
return result, nil
}Results:
| Metric | Sebelum | Sesudah | Improvement |
|---|---|---|---|
| Waktu akses riwayat transaksi | Visit branch (1-2 jam) | < 3 detik | ↓ 99% |
| Cetak mutasi di branch | ~4,200 request/bulan | ~1,100 request/bulan | ↓ 74% |
| Dispute resolution time | 3-5 hari kerja | < 24 jam | ↓ 80% |
🚀 5. Overall Business Impact
Implementasi SiTepat mengubah cara nasabah berinteraksi dengan bank:
Key Numbers
- Adoption rate: 680,000+ downloads di Play Store - Monthly Active Users: ~320,000 nasabah aktif per bulan - Call Center volume: turun 62% (dari ~8,500 → ~3,200 calls/bulan) - Late payment (lupa): turun dari 15% → 4% (↓73%) - NPL impact: kontribusi penurunan NPL dari 1.8% → 1.2% (bersama Tepati) - Branch visit reduction: estimasi ~45% nasabah tidak perlu lagi ke cabang untuk cek info rutin - Notification cost savings: Rp 0 vs rata-rata Rp 350/SMS × 320,000 nasabah = hemat ~Rp 112 juta/bulan
🏢 6. Post-Launch: Tantangan & Evolusi
6.1 Device Fragmentation Nightmare
Salah satu tantangan terbesar post-launch adalah fragmentasi device Android yang digunakan nasabah. Berbeda dengan Tepati yang perangkatnya di-standardisasi oleh bank, SiTepat berjalan di device pribadi nasabah:
| Segmen Device | Distribusi | Masalah |
|---|---|---|
| RAM < 2GB, Android 5-7 | ~35% | App sering di-kill oleh OS, push notification tidak sampai |
| RAM 2-3GB, Android 8-9 | ~40% | Relatif stabil, occasional ANR (Application Not Responding) |
| RAM > 3GB, Android 10+ | ~25% | Performa optimal, battery optimization kadang block background service |
Solusi yang diterapkan:
- WorkManager untuk scheduling notification — lebih reliable dibanding AlarmManager di Android 8+.
- Lite Mode: Mengurangi animasi dan image resolution untuk device low-end.
- Adaptive Network: Mendeteksi kualitas jaringan dan menyesuaikan payload size.
6.2 Adopsi Digital oleh Nasabah Mikro
Tantangan unik yang tidak ditemukan di kebanyakan app banking: nasabah mikro banyak yang baru pertama kali menggunakan aplikasi perbankan. Kolaborasi antara engineering dan product yang kami lakukan:
- Onboarding Tutorial: Flow interaktif step-by-step saat pertama buka app.
- Bahasa sederhana: Tidak menggunakan istilah teknis perbankan. Contoh: "Sisa Pinjaman" bukan "Outstanding Balance".
- Font size besar: Minimum 16sp untuk readability, mengingat banyak nasabah berusia 40+ tahun.
- Community Officer-assisted registration: Petugas lapangan membantu nasabah registrasi pertama kali (bridging Tepati dan SiTepat).
6.3 Play Store Compliance
Mempublikasikan aplikasi banking di Play Store memerlukan compliance tambahan:
- Financial Services Policy: Harus memenuhi Google Play's financial services requirements.
- Target API Level: Wajib update ke target API level terbaru setiap tahun — berimplikasi pada perubahan permission model dan background restrictions.
- Data Safety Section: Deklarasi transparan tentang data collection dan encryption.
🎓 7. Lessons Learned
What Went Well
- Circuit breaker pattern menyelamatkan UX saat Core Banking System mengalami maintenance window — nasabah tetap bisa lihat data dari cache.
- Push notification gratis menggantikan SMS berbayar — ROI langsung terasa di cost saving.
- ISO 8583 Bridge yang modular memudahkan penambahan use case baru (mutasi, transfer, dll) tanpa mengubah Core Banking.
- Audit logging yang comprehensive membantu tim fraud investigation resolve kasus dalam hitungan jam, bukan hari.
- Stale-while-revalidate caching membuat UX terasa instan meskipun Core Banking response time variabel.
What I'd Do Differently
- Mulai dengan GraphQL sejak awal untuk Financing Dashboard — terlalu banyak REST endpoint yang akhirnya return data yang mirip-mirip.
- Implementasi Feature Flags lebih awal — saat ini A/B testing dan gradual rollout masih cukup manual.
- Invest di synthetic monitoring untuk proactively detect Core Banking latency degradation sebelum nasabah merasakan.
- gRPC antara internal services — JSON serialization overhead terasa saat traffic peak.
- React Native New Architecture (Fabric + TurboModules) untuk mengurangi bridge overhead pada device low-end.
Technical Debt yang Masih Ada
- Migration ke Jetpack Compose untuk modul-modul native security.
- Implementasi offline mode yang lebih robust — saat ini fallback hanya dari cache, belum ada local database layer seperti Tepati.
- Standardisasi error taxonomy antara ISO 8583 response codes dan HTTP status codes.
- End-to-end encryption untuk chat support dalam app.
- Optimisasi app size — saat ini ~45MB, target < 30MB untuk nasabah dengan storage terbatas.
🎉 Conclusion
Project SiTepat bukan sekadar tentang membangun aplikasi banking. Ini adalah pembuktian bahwa teknologi legacy (1980-an) bisa hidup berdampingan dengan ekspektasi modern (2020-an) jika dijembatani dengan arsitektur yang tepat.
Sebagai engineer, mudah bagi kita untuk terjebak mengejar hype teknologi terbaru. Namun, SiTepat mengajarkan saya bahwa nilai tertinggi seorang engineer bukan pada seberapa "bleeding edge" stack yang dipakai, melainkan pada kemampuan memecahkan masalah nyata dengan batasan yang ada.
Engineering Philosophy
"The best technology disappears."
Bagi manajemen, aplikasi ini hanyalah alat efisiensi. Bagi pedagang pasar yang menjadi nasabah kami, aplikasi ini adalah ketenangan pikiran.
Mampu membangun bridge yang aman, cepat, dan reliable di antara dua dunia yang berbeda (Legacy vs Modern, Tech-Savvy vs Awam) adalah kontribusi profesional yang paling saya banggakan dalam karir saya sejauh ini.