Dalam konteks modul layanan yang saling berinteraksi, muncul pertanyaan yang tak terelakkan: Dengan aturan apa komunikasi berlangsung? Dalam produk TI, "kontrak" merupakan pemahaman formal tentang data apa yang mengalir di antara sistem dan bagaimana data tersebut ditransmisikan. Hal ini mencakup format data (JSON, Protobuf, dll.), elemen struktural (bidang, tipe data), protokol komunikasi (REST, gRPC, antrean pesan), dan spesifikasi lainnya.
Kontrak memastikan keterbukaan (semua orang tahu apa yang diterima dan dikirim), prediktabilitas (kami dapat memperbarui kontrak dan memelihara versi), dan keandalan (sistem kami tidak akan gagal jika kami membuat perubahan yang dikelola dengan baik).
Dalam praktiknya, meskipun semua orang berbicara tentang layanan mikro, “kontrak”, dan API, kita sering melihat orang menggunakan pendekatan: “Mengapa tidak membuat tabel bersama dalam database alih-alih membangun API?”
Oleh karena itu, meskipun penggunaan tabel bersama untuk pertukaran data mungkin tampak efisien dan dioptimalkan untuk hasil yang cepat, hal itu menimbulkan berbagai tantangan teknis dan organisasi dalam jangka panjang. Namun, ketika tim memilih tabel bersama untuk pertukaran data, mereka mungkin menghadapi banyak masalah selama implementasi.
Saat layanan berkomunikasi melalui REST/gRPC/GraphQL, layanan tersebut memiliki definisi formal: OpenAPI (Swagger), skema protobuf, atau skema GraphQL. Skema ini mendefinisikan secara terperinci sumber daya (titik akhir) mana yang tersedia, kolom mana yang diharapkan, jenisnya, dan format permintaan/respons. Saat 'tabel bersama' bertindak sebagai kontrak, tidak ada deskripsi formal: Tidak ada deskripsi formal kontrak; hanya skema tabel (DDL) yang tersedia dan itu pun tidak terdokumentasi dengan baik. Setiap modifikasi kecil pada struktur tabel (misalnya, menambahkan atau menghapus kolom, mengubah tipe data) dapat memengaruhi tim lain yang membaca atau menulis ke tabel ini.
Pembuatan versi API merupakan praktik yang normal: Kita mungkin memiliki v1, v2, dan seterusnya, dan kita dapat mempertahankan kompatibilitas mundur lalu secara bertahap memindahkan klien ke versi yang lebih baru. Untuk tabel basis data, kita hanya memiliki operasi DDL (misalnya, ALTER TABLE
), yang terkait erat dengan mesin DB tertentu dan memerlukan penanganan migrasi yang cermat.
Tidak ada sistem terpusat yang dapat mengirimkan peringatan kepada konsumen tentang perubahan skema yang mengharuskan mereka memperbarui kueri mereka. Akibatnya, transaksi "di bawah meja" dapat terjadi: Seseorang dapat memposting di obrolan, "Besok, kami akan mengubah kolom X menjadi Y", tetapi tidak ada jaminan semua orang akan siap tepat waktu.
Bila ada API yang didefinisikan dengan jelas, jelas siapa pemiliknya: layanan yang berfungsi sebagai penerbit API. Bila beberapa tim menggunakan tabel basis data yang sama, ada kebingungan tentang siapa yang berhak menentukan struktur dan kolom mana yang harus disimpan serta cara menafsirkannya. Akibatnya, tabel tersebut dapat menjadi "milik siapa pun", dan setiap perubahan menjadi pertanyaan: "Kita perlu memeriksa dengan tim lain itu untuk memastikan apakah mereka menggunakan kolom lama!"
Sulit untuk melacak siapa yang dapat membaca dan menulis ke tabel jika banyak tim memiliki akses ke DB. Ada kemungkinan layanan yang tidak sah dapat mengakses data meskipun data tersebut tidak ditujukan untuk mereka. Lebih mudah untuk mengelola masalah tersebut dengan API: Anda dapat mengontrol hak akses (siapa yang dapat memanggil metode mana), menggunakan autentikasi dan otorisasi, dan memantau siapa yang memanggil apa. Dengan tabel, semuanya jauh lebih rumit.
Segala modifikasi internal pada data (menata ulang indeks, mempartisi tabel, mengubah DB) menjadi masalah global. Jika tabel berfungsi sebagai antarmuka publik, pemilik tidak dapat membuat perubahan internal tanpa membahayakan semua pembaca dan penulis eksternal.
Ini adalah aspek yang paling menyakitkan: Bagaimana seseorang memberi tahu tim lain bahwa skema akan berubah keesokan harinya?
Bila beberapa tim menggunakan tabel bersama untuk memilih dan memperbarui data penting, hal itu dapat dengan mudah menjadi "medan perang". Hasilnya adalah logika bisnis tersebar di berbagai layanan, dan tidak ada kontrol terpusat atas integritas data. Menjadi sangat sulit untuk mengetahui mengapa bidang tertentu disimpan dengan cara tertentu, siapa yang dapat memperbaruinya, dan apa yang terjadi jika dibiarkan kosong.
Misalnya, misalkan tabel rusak: Katakanlah, ada data yang salah atau seseorang telah mengunci beberapa baris penting. Mengidentifikasi sumber masalah sering kali memerlukan pertanyaan kepada setiap tim yang memiliki akses DB untuk menentukan kueri apa yang menyebabkan masalah tersebut. Sering kali tidak jelas: Ini berarti kueri satu tim mungkin telah mengunci basis data, sementara kueri tim lain menghasilkan kesalahan yang dapat diamati.
Basis data bersama merupakan satu titik kegagalan. Jika basis data tersebut mati, maka banyak layanan akan ikut mati bersamanya. Jika basis data mengalami masalah kinerja karena permintaan yang besar dari satu layanan, semua layanan akan mengalami masalah. Dalam model dengan API dan kepemilikan data yang jelas, setiap tim menguasai ketersediaan dan kinerja layanan mereka, sehingga kegagalan pada satu komponen tidak akan menyebar ke komponen lainnya.
Kompromi yang umum adalah: “Kami akan memberi Anda replika baca-saja sehingga Anda dapat melakukan kueri tanpa memengaruhi basis data utama kami.” Awalnya, hal itu mungkin mengatasi beberapa masalah beban, tetapi:
Praktik desain modern (misalnya, “API First” atau “Contract First”) dimulai dengan definisi antarmuka formal. Skema OpenAPI/Swagger, protobuf, atau GraphQL digunakan. Dengan cara ini, baik orang maupun mesin mengetahui titik akhir mana yang tersedia, kolom mana yang wajib diisi, dan tipe data apa yang digunakan.
Dalam arsitektur layanan mikro (atau bahkan modular), asumsinya adalah bahwa setiap layanan memiliki datanya sendiri secara keseluruhan. Layanan tersebut mendefinisikan struktur, penyimpanan, dan logika bisnis serta menyediakan API untuk semua akses eksternal ke API tersebut. Tidak seorang pun dapat menyentuh basis data 'milik orang lain': hanya titik akhir atau peristiwa resmi. Hal ini memudahkan setiap kali ada perubahan yang dipertanyakan dan selalu jelas siapa yang harus disalahkan.
GET /items
, POST /items
, dll., dan klien membuat permintaan dengan skema data yang terdefinisi dengan baik (DTO).
Apa pun modelnya, penerapan kontrol versi pada antarmuka adalah mungkin dan penting. Misalnya:
Prinsip dasarnya adalah bahwa tim yang memiliki data berhak memutuskan cara menyimpan dan mengelolanya, tetapi mereka tidak boleh memberikan akses tulis langsung ke layanan lain. Layanan lain harus melalui API, bukan mengedit data asing. Hal ini menghasilkan distribusi tanggung jawab yang lebih jelas: Jika layanan A rusak, maka layanan A bertanggung jawab untuk memperbaikinya, bukan layanan di sekitarnya.
Sekilas, jika semuanya ada dalam satu tim, mengapa harus mempersulit dengan API? Kenyataannya, bahkan jika Anda memiliki satu produk yang dibagi menjadi beberapa modul, tabel bersama dapat menimbulkan masalah yang sama.
Misalnya, layanan Pesanan adalah pemilik tabel pesanan, dan layanan Penagihan tidak mengakses tabel tersebut secara langsung – layanan ini membuat panggilan ke titik akhir layanan Pesanan untuk mendapatkan detail pesanan atau menandai pesanan sebagai sudah dibayar.
Pada tingkat yang lebih tinggi, ketika dua atau lebih tim bertanggung jawab atas area yang berbeda, prinsip-prinsipnya tetap sama. Misalnya:
Jika Tim B secara langsung menanyakan tabel “Katalog” milik Tim A, setiap perubahan skema internal di A (misalnya, menambahkan kolom, mengubah struktur) dapat memengaruhi Tim B.
Pendekatan yang tepat adalah menggunakan API: Tim A menyediakan titik akhir seperti GET /catalog/items
, GET /catalog/items/{id}
, dll., dan Tim B menggunakan metode tersebut. Jika A mampu mendukung versi lama dan baru, mereka dapat merilis /v2, yang memberi waktu bagi B untuk bermigrasi.
Dengan kontrak formal, semua perubahan terlihat: di Swagger/OpenAPI, file .proto, atau dokumentasi acara. Setiap pembaruan dapat didiskusikan sebelumnya, diuji dengan benar, dan dijadwalkan, dengan strategi kompatibilitas mundur sesuai kebutuhan.
Perubahan dalam satu layanan berdampak lebih kecil pada layanan lainnya. Tim tidak perlu khawatir akan "merusak" orang lain jika mereka mengelola bidang atau titik akhir baru dan lama dengan baik, sehingga memastikan transisi yang lancar.
Gateway API, autentikasi, dan otorisasi (JWT, OAuth) merupakan standar untuk layanan, tetapi hampir mustahil dengan tabel bersama. Lebih mudah untuk menyempurnakan akses (siapa yang dapat memanggil metode mana), menyimpan log, melacak statistik penggunaan, dan memberlakukan kuota. Hal ini membuat sistem lebih aman dan lebih dapat diprediksi.
Tabel bersama dalam basis data merupakan detail implementasi, bukan kesepakatan antara layanan, sehingga tidak dianggap sebagai kontrak. Banyaknya masalah (versi yang rumit, perubahan yang kacau, kepemilikan yang tidak jelas, risiko keamanan, dan kinerja) membuat pendekatan ini tidak dapat dipertahankan dalam jangka panjang.
Pendekatan yang tepat adalah Contract First yang berarti mendefinisikan interaksi melalui desain formal dan mengikuti prinsip bahwa setiap layanan tetap menjadi pemilik datanya. Hal ini tidak hanya membantu mengurangi utang teknis tetapi juga meningkatkan transparansi, mempercepat pengembangan produk, dan memungkinkan perubahan yang aman tanpa harus terlibat dalam pemadaman kebakaran atas skema basis data.
Ini merupakan masalah teknis (bagaimana merancang dan mengintegrasikan) dan masalah organisasi (bagaimana tim berkomunikasi dan mengelola perubahan). Jika Anda ingin produk Anda berkembang tanpa harus berhadapan dengan keadaan darurat yang tak berkesudahan terkait skema basis data, maka Anda harus mulai berpikir dalam hal kontrak daripada akses basis data langsung.