Blog

  • Sistem Reservasi Travel dan Shuttle: Kursi, Jadwal, Manifest

    Sistem Reservasi Travel dan Shuttle: Kursi, Jadwal, Manifest

    Sistem reservasi travel dan shuttle pada intinya menyelesaikan tiga hal. Pertama, denah kursi per keberangkatan, jadi kursi 3 di jadwal jam 7 pagi hanya bisa dijual sekali dan tidak ada lagi cerita dua penumpang berebut kursi yang sama. Kedua, jadwal armada dan sopir tersusun rapi, siapa bawa mobil apa, jam berapa, rute mana, semua tercatat di satu tempat. Ketiga, manifest penumpang terbentuk otomatis, sopir buka HP langsung lihat daftar penumpang, urutan jemput, dan status bayar, sementara kantor pegang data yang sama tanpa perlu saling telepon.

    Kalau usaha travel Anda masih mengandalkan chat WA dan papan tulis di kantor, tiga hal di atas biasanya jadi sumber masalah harian. Artikel ini membahas kenapa cara manual mulai keteteran begitu armada bertambah, seperti apa alur reservasi yang rapi dari pesan sampai berangkat, dan kapan sebenarnya Anda belum perlu sistem sama sekali.

    Masalah booking manual yang kelihatan sepele tapi mahal

    Hampir semua usaha travel antar kota mulai dari cara yang sama. Penumpang chat WA, admin catat di buku atau papan tulis, sopir difoto daftar penumpangnya sebelum berangkat. Cara ini jalan, sampai suatu titik.

    Masalah pertama yang paling sering muncul adalah kursi dijual dobel. Dua admin pegang HP berbeda, dua penumpang booking kursi yang sama di jam yang sama. Ketahuannya pas hari H, di depan penumpang. Salah satu harus dipindah, dan yang kena biasanya penumpang yang datang belakangan. Sekali dua kali mungkin dimaafkan. Kalau sering, penumpang pindah ke travel sebelah.

    Masalah kedua, penumpang ganti jadwal lewat WA dan catatannya tercecer. Chat masuk jam 10 malam, admin baca tapi lupa update papan. Besoknya sopir jemput ke alamat orang yang sudah pindah ke keberangkatan sore. Satu penumpang salah, urutan jemput satu mobil ikut berantakan.

    Masalah lain menyusul. Sopir pegang catatan tulisan tangan yang kadang tidak kebaca. Titik jemput door to door cuma ditulis “perumahan blok C” tanpa detail, sopir muter mencari alamat sambil ditelepon penumpang lain yang menunggu. Lalu di akhir hari, rekap setoran sopir tidak jelas. Berapa penumpang yang bayar cash di mobil, berapa yang transfer, berapa yang masih DP. Kalau angkanya tidak ketemu, yang muncul bukan cuma selisih uang tapi juga rasa saling curiga antara kantor dan sopir.

    Satu per satu kelihatan sepele. Digabung setiap hari, ini yang membuat pemilik travel tidak bisa lepas dari HP bahkan saat libur.

    Alur reservasi yang rapi, dari pesan sampai berangkat

    Sistem reservasi pada dasarnya memindahkan alur yang sudah Anda jalankan ke bentuk yang tidak bergantung pada ingatan admin. Urutannya kurang lebih begini.

    • Pilih jadwal. Penumpang atau admin memilih tanggal, rute, dan jam keberangkatan. Jadwal yang penuh otomatis tertutup, tidak bisa dibooking lagi.
    • Pilih kursi. Denah kursi tampil sesuai armada yang dipakai. Kursi yang sudah terisi terkunci, jadi dobel booking tertutup dari awal, bukan dicegah pakai ingatan.
    • Isi data penumpang dan titik jemput. Nama, nomor HP, alamat jemput dan alamat antar untuk layanan door to door. Alamat bisa disertai titik peta atau catatan patokan, bukan sekadar nama perumahan.
    • Bayar DP atau lunas. Status pembayaran melekat di kursi. Admin dan sopir sama sama tahu kursi mana yang sudah lunas, mana yang baru DP, mana yang janji bayar di mobil.
    • E-ticket terkirim. Penumpang menerima bukti booking berisi jadwal, nomor kursi, dan titik jemput. Tidak ada lagi debat “kemarin saya bilangnya jam 1” karena semua tertulis.
    • Reminder H-1. Sehari sebelum berangkat penumpang menerima pengingat otomatis lewat WA. Penumpang lupa jadwal itu nyata, dan kursi kosong karena penumpang ketiduran adalah rugi yang bisa dicegah.

    Perhatikan satu hal. Alurnya sama dengan yang selama ini Anda kerjakan manual. Bedanya, setiap langkah tercatat sekali dan dibaca semua orang, bukan tersebar di chat pribadi beberapa admin.

    Manifest di HP sopir, bukan kertas yang difoto

    Bagian yang paling terasa di lapangan justru bukan halaman booking, tapi manifest untuk sopir. Begitu jadwal dikunci, sopir membuka HP dan melihat daftar penumpang untuk keberangkatannya. Isinya bukan sekadar nama.

    • Urutan jemput yang sudah disusun, dari alamat pertama sampai terakhir, jadi sopir tidak perlu menebak rute sendiri.
    • Nomor HP tiap penumpang, tinggal tap untuk menelepon kalau penumpang belum keluar rumah.
    • Status bayar per kursi. Sopir tahu persis dari siapa dia harus menerima uang di mobil dan berapa jumlahnya.

    Kalau ada perubahan mendadak, misalnya satu penumpang pindah ke keberangkatan sore, admin cukup mengubah di sistem dan manifest sopir ikut berubah saat itu juga. Tidak ada versi kertas yang sudah kadaluarsa di dashboard mobil.

    Reschedule dan pembatalan juga jadi punya aturan yang jelas. Anda tentukan sendiri kebijakannya, misalnya ganti jadwal gratis maksimal sehari sebelum berangkat, pembatalan di hari H memotong DP. Sistem tinggal menjalankan aturan itu secara konsisten. Admin tidak perlu berdebat dengan penumpang satu per satu, karena aturannya sama untuk semua orang dan tercatat sejak booking.

    Laporan yang membuat pemilik bisa ambil keputusan

    Selama data booking tersebar di chat dan papan tulis, pertanyaan sederhana pun sulit dijawab. Rute mana yang paling ramai. Jam keberangkatan mana yang sering kosong. Berapa sebenarnya pendapatan bulan lalu per armada.

    Begitu semua reservasi lewat satu sistem, laporan seperti ini muncul sendiri dari data harian.

    • Okupansi per rute dan per jam. Anda bisa lihat keberangkatan mana yang rata rata terisi 90 persen dan mana yang cuma 40 persen. Dari sini keputusan menambah atau menggeser jadwal jadi berdasar angka, bukan perasaan.
    • Rute paling menguntungkan. Rute ramai belum tentu paling untung kalau jaraknya jauh dan biaya operasionalnya tinggi. Laporan per rute membantu melihat mana yang benar benar layak ditambah armadanya.
    • Setoran per armada dan per sopir. Uang cash yang diterima sopir tercatat per kursi, jadi rekap setoran di akhir hari tinggal dicocokkan, bukan dihitung ulang dari ingatan.

    Kalau Anda bekerja sama dengan agen atau mitra penjual tiket, misalnya warung atau counter di terminal, penjualan mereka juga bisa masuk lewat sistem yang sama. Komisi tercatat otomatis per tiket, jadi hitungan di akhir bulan tidak lagi mengandalkan catatan dua pihak yang bisa beda angka.

    Pola pencatatan seperti ini sebenarnya sama dengan sistem operasional usaha lain yang pernah kami kerjakan. Kalau mau lihat gambaran umumnya, kami menulis lebih lengkap di halaman sistem aplikasi custom.

    Kapan booking manual masih cukup

    Jujur saja, tidak semua usaha travel butuh sistem hari ini. Kalau Anda baru jalan dengan satu armada, satu rute, dan penumpangnya kebanyakan langganan yang itu itu saja, buku catatan dan WA masih sangat bisa diandalkan. Jumlah keberangkatan per hari sedikit, admin hafal semua penumpang, risiko kursi dobel kecil.

    Tanda bahwa Anda mulai butuh sistem biasanya muncul bertahap. Armada jadi dua atau tiga. Admin lebih dari satu orang dan mulai saling tumpuk catatan. Keluhan kursi dobel atau salah jemput muncul lebih dari sekali sebulan. Anda sendiri mulai tidak yakin dengan angka setoran harian. Di titik itu, biaya membenahi sistem hampir selalu lebih murah daripada biaya penumpang yang kecewa lalu pindah.

    Satu catatan lagi. Sistem reservasi travel paling nyaman dipakai kalau sopir mengaksesnya dari HP, bukan dari komputer kantor. Karena itu banyak yang membangunnya sekalian dalam bentuk aplikasi mobile untuk sopir dan penumpang. Soal ini kami bahas di halaman jasa pembuatan aplikasi mobile.

    Pertanyaan yang sering muncul

    Penumpang saya kebanyakan orang tua, tidak paham aplikasi. Bagaimana?

    Tidak masalah. Booking tetap bisa lewat admin seperti biasa. Penumpang chat WA atau telepon, admin yang memasukkan ke sistem, penumpang tinggal terima e-ticket. Yang berubah bukan cara penumpang memesan, tapi cara kantor mencatat. Kanal aplikasi untuk penumpang bisa ditambahkan belakangan kalau memang dibutuhkan.

    Bagaimana dengan penumpang langganan yang biasa dapat kursi tertentu?

    Kebiasaan ini justru gampang diakomodasi. Admin bisa menandai kursi favorit pelanggan tetap atau memblokir kursi tertentu sebelum dibuka untuk umum. Data langganan juga tersimpan, jadi booking berikutnya tidak perlu tanya ulang alamat jemput. Langganan tetap merasa diistimewakan, tanpa admin harus menghafal.

    Travel saya juga terima kirim paket barang. Bisa masuk sistem yang sama?

    Bisa. Paket dicatat seperti penumpang, menempel di keberangkatan tertentu, lengkap dengan pengirim, penerima, dan status pembayaran. Sopir melihatnya di manifest yang sama, jadi tidak ada paket ketinggalan di kantor.

    Kalau Anda sedang menimbang apakah usaha travel Anda sudah waktunya pindah dari papan tulis ke sistem, ceritakan saja dulu kondisi operasional Anda lewat halaman kontak kami. Diskusi awal tidak dipungut biaya, dan kalau ternyata cara manual masih cukup, kami akan bilang begitu.

  • Aplikasi Keuangan Event Organizer: Tahu Laba Rugi per Event

    Aplikasi Keuangan Event Organizer: Tahu Laba Rugi per Event

    Aplikasi keuangan event organizer menjawab satu pertanyaan yang jarang bisa dijawab owner EO: event kemarin itu untung berapa persisnya. Caranya sederhana. Semua pemasukan dan pengeluaran dicatat per event, bukan dicampur di satu rekening. DP klien masuk ke catatan event A. Fee crew keluar dari catatan event A. Biaya vendor tercatat di event A juga. Begitu event selesai, angka laba ruginya langsung kelihatan tanpa perlu bongkar mutasi rekening tiga bulan ke belakang.

    Kalau selama ini Anda cuma bisa lihat saldo naik turun tanpa tahu event mana yang menyumbang untung dan event mana yang diam diam rugi, artikel ini untuk Anda. Kita bahas kenapa laporan keuangan EO sering menipu, apa saja yang perlu dicatat per event, dan kapan spreadsheet sebenarnya masih cukup.

    Kenapa laporan keuangan EO sering menipu

    Bisnis EO punya pola uang yang beda dari toko. Toko jual barang hari ini, uang masuk hari ini. EO tidak begitu. Satu event bisa punya tiga sampai lima kali pembayaran: DP saat booking, termin di tengah, pelunasan menjelang atau sesudah hari H. Semua masuk ke rekening yang sama, di tanggal yang berbeda beda, bercampur dengan pembayaran event lain.

    Akibatnya saldo rekening bulan ini bukan cerminan kinerja bulan ini. Bisa jadi saldo terlihat gemuk karena DP tiga event yang baru akan jalan dua bulan lagi. Uangnya kelihatan ada, padahal itu uang yang masih punya kewajiban di belakangnya: bayar vendor, bayar crew, sewa perlengkapan.

    Sisi pengeluaran lebih berantakan lagi. Vendor dibayar transfer dari rekening usaha. Kasbon crew keluar dari kas tunai di lapangan. Perlengkapan mendadak dibeli pakai uang pribadi lalu diganti belakangan. Tiga jalur uang, tiga catatan yang terpisah, dan sering kali satu di antaranya tidak dicatat sama sekali.

    Dari kombinasi itu lahir penyakit klasik EO: uang event A kepakai untuk nutup kebutuhan event B tanpa ada yang sadar. Semua terasa aman selama order terus masuk. Begitu ada satu dua bulan sepi, baru ketahuan ada lubang yang selama ini ditambal pakai DP event berikutnya.

    Pencatatan per event: fondasi laba rugi yang jujur

    Inti dari aplikasi keuangan EO bukan fiturnya yang canggih. Intinya satu prinsip: setiap rupiah harus punya alamat event. Praktiknya, setiap event jadi satu buku tersendiri yang memuat dua sisi.

    Sisi pemasukan berisi nilai kontrak dan rincian terminnya. Misalnya DP 30 persen, termin kedua 40 persen, pelunasan 30 persen. Setiap termin punya jumlah, tanggal jatuh tempo, dan status: sudah dibayar, menunggu, atau lewat jatuh tempo.

    Sisi pengeluaran berisi semua biaya yang menempel ke event itu:

    • Pembayaran vendor seperti katering, dekorasi, dokumentasi, atau sound system
    • Sewa atau pembelian perlengkapan khusus untuk event tersebut
    • Transport dan akomodasi tim
    • Fee crew, termasuk crew lepas harian
    • Kasbon dan pengeluaran kecil di lapangan

    Begitu dua sisi ini rapi, laba rugi per event tinggal soal pengurangan. Bukan lagi perkiraan “kayaknya untung” tapi angka yang bisa dipertanggungjawabkan. Owner jadi bisa membandingkan: wedding di gedung marginnya berapa, gathering kantor marginnya berapa, event komunitas layak diambil atau tidak.

    Kalau bisnis Anda juga butuh alur booking sampai serah terima yang rapi, itu ranah aplikasi manajemen order event organizer yang idealnya terintegrasi dengan modul keuangan ini.

    Payment tracking klien: penyelamat cashflow

    Ada satu penyakit yang hampir semua EO pernah alami: event sudah jalan, bahkan sudah selesai, tapi pelunasan belum ditagih. Bukan karena klien nakal. Karena tidak ada yang ingat. Owner sibuk pegang event berikutnya, admin mengira sudah lunas, dan invoice pelunasan baru dikirim sebulan kemudian dengan nada tidak enak.

    Payment tracking menutup lubang ini dengan tiga hal sederhana. Pertama, jadwal termin tercatat sejak kontrak diteken, jadi sistem tahu kapan setiap tagihan seharusnya masuk. Kedua, reminder jatuh tempo, baik untuk tim internal yang harus menagih maupun sebagai dasar mengirim invoice ke klien tepat waktu. Ketiga, status yang jelas per termin, jadi satu kali lihat langsung ketahuan klien mana yang masih punya sisa kewajiban.

    Efeknya langsung terasa di cashflow. Menagih tepat waktu itu bedanya bisa dua sampai empat minggu dibanding menagih setelah ingat. Untuk EO yang sedang pegang beberapa event paralel, selisih waktu itu sering jadi penentu bisa bayar vendor tepat waktu atau harus mengulur.

    Pengeluaran crew dan kasbon: dicatat saat kejadian, bukan dari ingatan

    Hari H event itu kacau. Crew minta kasbon buat makan siang tim. Ada kabel rusak yang harus beli saat itu juga. Tambahan transport karena venue pindah gate. Semua keluar dari kas tunai atau kantong siapa pun yang pegang uang, dan janji dicatat nanti.

    Masalahnya, “nanti” itu biasanya seminggu kemudian. Rekonsiliasi jadi acara mencocokkan ingatan: ini nota apa, yang ambil siapa, sudah diganti atau belum. Selisih lima puluh ribu di sana, seratus ribu di sini, dikali sekian event setahun, jumlahnya tidak kecil. Dan semua selisih itu menggerus margin tanpa pernah muncul di laporan.

    Solusinya bukan disiplin yang lebih keras, tapi alat yang lebih dekat. Kalau pencatatan bisa dilakukan lewat HP saat kejadian, pilih event, tulis jumlah, foto nota, selesai, maka data yang sampai ke owner adalah data kejadian, bukan data ingatan. Kasbon crew juga jadi jelas statusnya: siapa pegang berapa, sudah dipertanggungjawabkan atau belum.

    Dashboard owner: dari lihat saldo jadi ambil keputusan

    Kalau pencatatan per event sudah jalan, dashboard owner berubah fungsi. Bukan lagi sekadar angka saldo, tapi alat bantu keputusan. Minimal ada tiga hal yang harus terjawab sekali buka.

    Pertama, cashflow bulan berjalan. Berapa yang sudah masuk, berapa tagihan yang akan jatuh tempo, berapa kewajiban ke vendor dan crew yang harus keluar. Kedua, laba rugi per event yang sedang dan baru selesai jalan. Ketiga, pola jangka panjang: jenis event mana yang marginnya konsisten bagus dan layak diperbanyak, mana yang ramai tapi tipis.

    Bagian ketiga ini yang paling jarang dimiliki EO, padahal paling menentukan arah bisnis. Tanpa data per event, keputusan ambil atau tolak order cuma berdasar feeling dan kapasitas tim. Dengan data per event, owner bisa bilang dengan yakin: gathering korporat margin sekian persen, kita kejar segmen itu tahun depan.

    Studi kasus: Mitra Infinite

    Pola masalah di atas persis yang dialami Mitra Infinite, bisnis event organizer dan wedding organizer yang kami bantu bangun sistemnya. Sebelum pakai sistem, data order tercecer di chat dan pencatatan manual. Owner kesulitan mendapat visibilitas cepat atas status pembayaran dan arus kas.

    Di sisi keuangan, sistem yang kami bangun menyusun tracking pembayaran supaya tagihan klien tidak ada yang terlewat, plus dashboard monitoring untuk keputusan operasional harian. Sistemnya berjalan sebagai aplikasi mobile berbasis Flutter dan web admin, dengan notification service agar hal penting tidak menunggu dibuka manual. Detail lengkapnya bisa dibaca di studi kasus Mitra Infinite.

    Poin yang paling relevan buat pembahasan kita: tracking pembayaran dan visibilitas arus kas bukan fitur pelengkap di sistem itu. Keduanya termasuk alasan utama sistem dibangun, karena di bisnis event, uang yang tidak tertagih dan arus kas yang tidak terbaca itu masalah harian, bukan masalah akhir tahun.

    Kapan spreadsheet masih cukup

    Jujur saja, tidak semua EO butuh aplikasi custom hari ini. Kalau Anda pegang satu sampai dua event per bulan, keuangan dipegang satu orang yang disiplin, dan Anda sanggup konsisten mengisi satu sheet per event, spreadsheet masih cukup. Prinsip pencatatan per event bisa jalan di Google Sheets, dan itu jauh lebih baik daripada tidak mencatat sama sekali.

    Spreadsheet mulai jebol saat tiga kondisi ini muncul bersamaan: event berjalan paralel lebih dari dua tiga sekaligus, banyak orang yang pegang uang di lapangan, dan Anda mulai sering kecolongan tagihan atau menemukan selisih kas yang tidak bisa dijelaskan. Di titik itu masalahnya bukan template kurang bagus, tapi spreadsheet memang tidak dirancang untuk banyak tangan yang mencatat dari lapangan secara real time.

    Kalau Anda sudah di titik itu dan ingin sistem yang mengikuti alur bisnis sendiri, termin pembayaran versi Anda, struktur fee crew versi Anda, tim kami di Arrazy bisa bantu memetakan kebutuhannya lewat jasa pembuatan aplikasi mobile. Mulai dari diskusi alur dulu saja, tidak harus langsung bicara pengembangan.

    Pertanyaan yang sering muncul

    Apa bedanya dengan software akuntansi biasa?

    Software akuntansi umum dirancang untuk laporan standar seperti neraca dan laba rugi perusahaan secara keseluruhan. Pemisahan per event biasanya harus diakali lewat tag atau kategori manual, dan alur khas EO seperti termin pembayaran klien, kasbon crew, dan pencatatan lapangan lewat HP tidak jadi fokusnya. Aplikasi keuangan EO dibangun mengikuti alur itu sejak awal.

    Apakah cocok juga untuk wedding organizer?

    Cocok, bahkan sering lebih terasa manfaatnya. Siklus WO panjang, dari booking sampai hari H bisa setahun, dengan termin pembayaran yang lebih banyak. Makin panjang siklusnya, makin besar risiko termin terlewat kalau hanya mengandalkan ingatan.

    Apakah modul keuangan harus jadi satu dengan sistem order?

    Idealnya iya. Data keuangan menempel ke data event, jadi kalau order dan keuangan hidup di dua sistem terpisah, ada pekerjaan ganda memindahkan data dan risiko angka tidak sinkron. Yang umum kami sarankan: mulai dari modul yang paling sakit dulu, lalu modul lain menyusul di sistem yang sama.

  • Belajar Golang dari Nol #23: Transaksi Database dan Migrasi Skema

    Belajar Golang dari Nol #23: Transaksi Database dan Migrasi Skema

    Di Belajar Golang dari Nol #22 kita keluar rumah sebentar. Kita belajar memanggil API pihak ketiga dengan HTTP client. Sekarang kita pulang lagi ke database. Ada dua pekerjaan rumah yang belum selesai sejak bagian 12. Dua-duanya baru terasa sakit saat aplikasi kamu benar-benar dipakai orang. Kalau kamu baru bergabung di tengah seri, cek dulu daftar lengkap seri supaya tidak ada yang terlewat.

    Dua Masalah yang Belum Kita Jawab

    Di bagian 12 kita sudah bisa insert, update, dan query lewat database/sql. Di bagian 16 kita rapikan dengan repository pattern. Cukup untuk latihan. Belum cukup untuk production. Ada dua lubang besar.

    Masalah pertama: operasi berangkai yang harus semua-atau-tidak-sama-sekali. Contoh paling klasik ada di aplikasi toko. Saat ada order masuk, kamu harus memotong stok produk dan mencatat order baru. Dua query berbeda. Kalau query pertama sukses lalu query kedua gagal, datamu rusak. Stok sudah berkurang, tapi ordernya tidak ada.

    Masalah kedua: mengubah struktur tabel di database yang sudah berisi data. Di bagian 12 kita membuat tabel lewat CREATE TABLE IF NOT EXISTS di kode Go. Itu jalan sekali, lalu diam selamanya. Begitu aplikasi live dan kamu butuh kolom baru, cara itu tidak menolong sama sekali. Kamu tidak mungkin drop tabel yang isinya data pelanggan.

    Jawaban untuk masalah pertama adalah transaksi. Jawaban untuk masalah kedua adalah migrasi skema. Kita bahas satu per satu.

    Kenapa Transaksi Itu Penting

    Cerita nyata dulu. Bayangkan API toko online kamu menerima order. Kodenya menjalankan dua langkah: potong stok, lalu insert order. Suatu malam, koneksi database putus sepersekian detik. Tepat di antara dua langkah itu. Stok kaos sudah terpotong satu. Order gagal tercatat. Pembeli tidak dapat apa-apa, tapi stok di sistem berkurang.

    Kejadian seperti ini tidak muncul di laptop kamu saat development. Ia muncul di production, jam dua pagi, saat traffic sedang ramai. Selisihnya kecil, satu dua unit. Tapi menumpuk. Akhir bulan tim gudang bingung. Stok di sistem bilang habis, stok fisik masih ada. Ini yang biasa disebut stok bocor.

    Transaksi menutup lubang ini. Konsepnya sederhana. Kamu membungkus beberapa query jadi satu paket. Paket itu hanya punya dua kemungkinan akhir. Semua query berhasil dan disimpan permanen, itu namanya commit. Atau salah satu gagal dan semuanya dibatalkan seolah tidak pernah terjadi, itu namanya rollback. Tidak ada kondisi setengah jadi.

    Anatomi Transaksi di Go

    Di database/sql, transaksi dimulai dengan db.BeginTx. Fungsi ini menerima context, jadi ilmu dari bagian 17 langsung terpakai. Kalau request dibatalkan atau timeout, transaksinya ikut dibatalkan. Setelah itu semua query dijalankan lewat objek tx, bukan db.

    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()
    
    // jalankan query lewat tx, bukan db
    _, err = tx.ExecContext(ctx, "UPDATE produk SET stok = stok - 1 WHERE id = ?", 1)
    if err != nil {
        return err
    }
    
    return tx.Commit()
    

    Perhatikan baris defer tx.Rollback(). Ini pola paling penting di sini. Ditulis sekali, tepat setelah BeginTx berhasil. Logikanya begini. Kalau ada error di tengah dan fungsi return lebih awal, defer memastikan rollback tetap jalan. Transaksi tidak menggantung dan mengunci baris di database.

    Lalu bagaimana kalau semuanya sukses sampai Commit? Bukankah defer tetap memanggil Rollback setelahnya? Benar, tetap dipanggil. Tapi tidak berbahaya. Transaksi yang sudah di-commit statusnya sudah selesai. Rollback setelah commit hanya mengembalikan error sql.ErrTxDone dan tidak melakukan apa-apa ke database. Karena kita tidak memeriksa nilai baliknya, error itu lewat begitu saja. Satu baris defer, dua skenario aman.

    Contoh Utuh: BuatOrder dalam Satu Transaksi

    Sekarang kita gabungkan jadi method repository yang layak production. Method ini memotong stok dan mencatat order dalam satu transaksi. Kalau stok kurang, semuanya batal.

    var ErrStokKurang = errors.New("stok tidak mencukupi")
    
    type OrderRepository struct {
        db *sql.DB
    }
    
    func (r *OrderRepository) BuatOrder(ctx context.Context, produkID int64, jumlah int) (int64, error) {
        tx, err := r.db.BeginTx(ctx, nil)
        if err != nil {
            return 0, fmt.Errorf("mulai transaksi: %w", err)
        }
        defer tx.Rollback()
    
        // langkah 1: potong stok, hanya kalau stok cukup
        res, err := tx.ExecContext(ctx,
            "UPDATE produk SET stok = stok - ? WHERE id = ? AND stok >= ?",
            jumlah, produkID, jumlah,
        )
        if err != nil {
            return 0, fmt.Errorf("potong stok: %w", err)
        }
    
        baris, err := res.RowsAffected()
        if err != nil {
            return 0, err
        }
        if baris == 0 {
            // tidak ada baris berubah, artinya stok kurang.
            // return di sini memicu defer tx.Rollback()
            return 0, ErrStokKurang
        }
    
        // langkah 2: catat order
        hasil, err := tx.ExecContext(ctx,
            "INSERT INTO orders (produk_id, jumlah, status) VALUES (?, ?, 'baru')",
            produkID, jumlah,
        )
        if err != nil {
            return 0, fmt.Errorf("simpan order: %w", err)
        }
    
        orderID, err := hasil.LastInsertId()
        if err != nil {
            return 0, err
        }
    
        if err := tx.Commit(); err != nil {
            return 0, fmt.Errorf("commit: %w", err)
        }
    
        return orderID, nil
    }
    

    Ikuti alur gagalnya. Misal stok tinggal 2 dan pembeli minta 5. Query update tidak menemukan baris yang cocok karena syarat stok >= 5 tidak terpenuhi. RowsAffected mengembalikan 0. Fungsi return dengan ErrStokKurang. Defer menjalankan rollback. Insert order tidak pernah terjadi, dan stok tidak tersentuh. Database kembali bersih, seolah request itu tidak pernah datang.

    Dua Pembeli, Satu Barang Terakhir

    Ada satu jebakan lagi yang perlu kamu tahu, walau kita bahas ringan saja. Bayangkan stok tinggal satu, lalu dua request masuk bersamaan. Kalau kodemu bergaya cek dulu baru update, dua-duanya bisa lolos. Request A membaca stok, dapat 1. Sebelum A sempat update, request B juga membaca stok, dapat 1 juga. Keduanya merasa berhak. Keduanya memotong stok. Hasil akhir stok jadi minus satu, dan dua orang membeli barang yang cuma ada satu.

    Masalah semacam ini disebut race condition di level database, dan topik isolasi transaksi bisa jadi satu artikel sendiri. Untungnya, untuk kasus stok ada solusi praktis yang sudah kamu lihat di kode tadi. Jangan pisahkan cek dan update. Gabungkan keduanya dalam satu query atomik:

    UPDATE produk SET stok = stok - ? WHERE id = ? AND stok >= ?
    

    Database menjamin satu statement UPDATE dieksekusi utuh, satu per satu terhadap baris yang sama. Request pertama yang sampai akan memotong stok. Request kedua menemukan syarat stok >= ? sudah tidak terpenuhi, sehingga RowsAffected bernilai 0 dan order ditolak rapi. RowsAffected sendiri sudah kamu kenal sejak bagian 12. Sekarang dia naik pangkat, dari sekadar info jadi penjaga gerbang.

    Masalah Kedua: Skema yang Ikut Bertumbuh

    Aplikasi yang hidup pasti berubah. Bulan ini tabel produk cukup dengan nama, harga, dan stok. Bulan depan tim minta kolom foto. Di sinilah pendekatan bagian 12 mentok. CREATE TABLE IF NOT EXISTS hanya bekerja saat tabel belum ada. Kalau tabel sudah ada, statement itu diam saja. Dia tidak bisa menambah kolom, tidak bisa mengubah tipe data, tidak bisa apa-apa.

    Solusi naifnya mengerikan: drop tabel lalu buat ulang. Di development mungkin tidak apa-apa. Di production itu artinya menghapus seluruh data pelanggan. Jelas bukan pilihan.

    Solusi yang dipakai industri adalah migrasi skema. Idenya begini. Setiap perubahan struktur database ditulis sebagai file SQL bernomor urut. File pertama membuat tabel. File kedua menambah kolom. File ketiga menambah index. Dan seterusnya. Setiap perubahan punya pasangan: file up untuk menerapkan perubahan, dan file down untuk membatalkannya. Database mencatat sudah sampai nomor berapa dia bermigrasi. Jadi database lama dan database baru bisa dibawa ke struktur yang sama, cukup dengan menjalankan migrasi yang belum dieksekusi.

    Praktik dengan golang-migrate

    Tool paling populer di ekosistem Go untuk urusan ini adalah golang-migrate. Kita pakai versi CLI-nya. Install dulu:

    go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
    

    Lalu buat folder migrasi dan file pertamanya:

    mkdir -p db/migrations
    migrate create -ext sql -dir db/migrations -seq buat_tabel_produk
    

    Perintah itu menghasilkan dua file kosong dengan format nama yang penting kamu pahami: 000001_buat_tabel_produk.up.sql dan 000001_buat_tabel_produk.down.sql. Angka di depan adalah urutan eksekusi. Isi file up dengan struktur tabel kita:

    -- 000001_buat_tabel_produk.up.sql
    CREATE TABLE produk (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        nama VARCHAR(100) NOT NULL,
        harga INT NOT NULL,
        stok INT NOT NULL DEFAULT 0
    );
    
    CREATE TABLE orders (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        produk_id BIGINT NOT NULL,
        jumlah INT NOT NULL,
        status VARCHAR(20) NOT NULL,
        dibuat_pada TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (produk_id) REFERENCES produk(id)
    );
    

    File down berisi kebalikannya. Urutannya dibalik karena orders bergantung pada produk:

    -- 000001_buat_tabel_produk.down.sql
    DROP TABLE orders;
    DROP TABLE produk;
    

    Jalankan migrasinya:

    migrate -path db/migrations \
      -database "mysql://user:rahasia@tcp(localhost:3306)/toko" up
    

    Kalau kamu intip databasenya sekarang, ada satu tabel tambahan bernama schema_migrations. Tabel ini milik golang-migrate. Isinya nomor versi terakhir yang sudah diterapkan. Berkat tabel ini, menjalankan migrate up dua kali aman. Migrasi yang sudah jalan tidak diulang.

    Sekarang skenario kolom baru. Di bagian 20 kita belajar upload file, dan produk kita butuh kolom untuk menyimpan nama file fotonya. Jangan sentuh file 000001. Buat migrasi kedua:

    migrate create -ext sql -dir db/migrations -seq tambah_kolom_foto
    
    -- 000002_tambah_kolom_foto.up.sql
    ALTER TABLE produk ADD COLUMN foto VARCHAR(255) NULL;
    
    -- 000002_tambah_kolom_foto.down.sql
    ALTER TABLE produk DROP COLUMN foto;
    

    Jalankan migrate up lagi. Golang-migrate melihat database sudah di versi 1, jadi hanya file 000002 yang dieksekusi. Data produk lama tetap utuh, hanya bertambah satu kolom kosong. Kalau ternyata ada masalah, migrate down 1 mengembalikan satu langkah ke belakang lewat file down.

    Aturan Emas Migrasi di Production

    Tiga aturan ini pendek, tapi menyelamatkan banyak database.

    • Jangan pernah edit migrasi lama yang sudah jalan. File migrasi yang sudah diterapkan itu sejarah. Kalau butuh perubahan, selalu bikin file migrasi baru. Mengedit file lama membuat database yang sudah bermigrasi dan yang belum jadi punya struktur berbeda, dan itu sulit sekali dilacak.
    • Backup dulu sebelum migrate di production. File down membantu, tapi ada perubahan yang tidak bisa dibalik. DROP COLUMN yang sudah jalan tidak bisa mengembalikan isi kolomnya. Rutinitas mysqldump yang kita bahas di bagian 15 adalah jaring pengamanmu di sini.
    • Jalankan migrasi sebagai bagian dari deploy. Urutannya: backup, migrate up, baru jalankan versi aplikasi terbaru. Dengan begitu kode baru tidak pernah bertemu skema lama.

    Latihan: Rangkai Semuanya

    Waktunya latihan utuh. Susun project kecil dengan struktur ini: dua file migrasi seperti di atas di folder db/migrations, lalu OrderRepository dengan method BuatOrder yang sudah kita tulis. Sambungkan ke handler HTTP sederhana:

    func (h *OrderHandler) Buat(w http.ResponseWriter, r *http.Request) {
        var req struct {
            ProdukID int64 `json:"produk_id"`
            Jumlah   int   `json:"jumlah"`
        }
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
            http.Error(w, "body tidak valid", http.StatusBadRequest)
            return
        }
    
        orderID, err := h.repo.BuatOrder(r.Context(), req.ProdukID, req.Jumlah)
        if errors.Is(err, ErrStokKurang) {
            http.Error(w, "stok tidak mencukupi", http.StatusConflict)
            return
        }
        if err != nil {
            http.Error(w, "terjadi kesalahan server", http.StatusInternalServerError)
            return
        }
    
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]int64{"order_id": orderID})
    }
    

    Lalu uji alurnya dengan curl. Pertama, jalankan migrasi dan isi satu produk dengan stok 3. Kemudian coba order yang melebihi stok:

    curl -X POST http://localhost:8080/orders \
      -H "Content-Type: application/json" \
      -d '{"produk_id": 1, "jumlah": 10}'
    

    Kamu akan menerima status 409 dengan pesan stok tidak mencukupi. Sekarang cek stoknya lewat endpoint produk atau langsung di database. Stok masih 3, tidak tersentuh sama sekali. Itu bukti rollback bekerja. Terakhir, order dengan jumlah 2. Order tercatat, stok jadi 1, dan keduanya berubah bersama dalam satu transaksi. Coba juga tembak dua request bersamaan saat stok tinggal satu, dan lihat sendiri hanya satu yang berhasil.

    Penutup

    Hari ini kamu menutup dua lubang production yang paling sering menggigit pemula. Transaksi memastikan operasi berangkai selesai utuh atau batal total, dengan pola BeginTx, defer tx.Rollback(), dan Commit. Migrasi skema membuat struktur database bisa bertumbuh bersama aplikasimu tanpa mengorbankan data, dengan file up dan down yang berurutan dan tercatat rapi di schema_migrations.

    Di bagian 24 kita naik satu level lagi soal performa. Judulnya “Caching dengan Redis: Bikin API Terasa Instan”. Sampai ketemu di sana.

    Kalau kamu sedang membangun aplikasi yang datanya tidak boleh bocor, misalnya sistem inventaris atau POS dengan stok yang harus akurat, tim kami terbiasa menangani kasus seperti ini. Lihat layanan pengembangan sistem aplikasi Arrazy untuk cerita lengkapnya.

  • Belajar Database dari Nol #4: Tipe Data MySQL yang Tepat

    Belajar Database dari Nol #4: Tipe Data MySQL yang Tepat

    Memilih tipe data MySQL yang tepat itu sederhana kalau pegang empat aturan dasar. Bilangan bulat pakai INT, uang pakai DECIMAL dan jangan pernah FLOAT, teks pendek pakai VARCHAR, lalu tanggal dan waktu pakai DATE atau DATETIME. Sisanya adalah soal memahami kapan aturan dasar itu perlu disesuaikan, dan itulah isi tutorial ini.

    Ini bagian keempat dari seri Belajar Database dari Nol. Di bagian sebelumnya kita sudah membuat database dan tabel pertama. Sekarang kita bedah kolom demi kolom: kenapa tipe data yang salah bisa bikin saldo pelanggan meleset, teks terpotong, atau jam transaksi bergeser tujuh jam.

    Prasyarat Sebelum Mulai

    Tutorial ini memakai MySQL 8.4 yang sudah kamu install di bagian pertama seri. Kamu juga perlu paham cara membuat database dan tabel. Kalau belum, selesaikan dulu Belajar Database dari Nol #3: Membuat Database & Tabel MySQL karena semua contoh di sini dibangun dari perintah CREATE TABLE.

    Siapkan database latihan supaya percobaan kita tidak mengganggu tabel lain:

    CREATE DATABASE IF NOT EXISTS latihan_tipe;
    USE latihan_tipe;

    Tipe Data Angka: INT, BIGINT, dan DECIMAL

    MySQL punya beberapa tipe bilangan bulat. Bedanya cuma satu: seberapa besar angka yang bisa ditampung, dan berapa byte yang dipakai per baris.

    Tipe Ukuran Rentang (signed) Rentang (unsigned)
    TINYINT 1 byte -128 s.d. 127 0 s.d. 255
    SMALLINT 2 byte -32.768 s.d. 32.767 0 s.d. 65.535
    INT 4 byte -2.147.483.648 s.d. 2.147.483.647 0 s.d. 4.294.967.295
    BIGINT 8 byte kira-kira -9,2 kuintiliun s.d. 9,2 kuintiliun 0 s.d. 18,4 kuintiliun

    Aturan praktisnya: INT cukup untuk hampir semua kebutuhan, termasuk primary key tabel yang isinya jutaan baris. BIGINT baru dibutuhkan kalau kamu yakin barisnya bakal melewati 2,1 miliar, misalnya tabel log atau tabel transaksi sistem besar. Jangan pakai BIGINT untuk semua kolom hanya karena “biar aman”, karena setiap baris jadi lebih boros 4 byte per kolom dan index ikut membengkak.

    Tambahkan UNSIGNED kalau nilainya tidak mungkin negatif, misalnya stok atau id. Rentang positifnya jadi dua kali lipat:

    CREATE TABLE produk (
      id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
      nama VARCHAR(100) NOT NULL,
      stok SMALLINT UNSIGNED NOT NULL DEFAULT 0
    );

    Kenapa Uang Jangan Pakai FLOAT atau DOUBLE

    FLOAT dan DOUBLE menyimpan angka secara biner dengan presisi terbatas. Banyak pecahan desimal seperti 0,1 tidak bisa diwakili secara persis dalam biner, jadi yang tersimpan adalah nilai pendekatan. Buktikan sendiri:

    CREATE TABLE demo_float (saldo FLOAT);
    INSERT INTO demo_float VALUES (0.1), (0.2);
    SELECT SUM(saldo) FROM demo_float;

    Hasilnya bukan 0,3:

    +---------------------+
    | SUM(saldo)          |
    +---------------------+
    | 0.30000000447034836 |
    +---------------------+

    Selisihnya kelihatan kecil, tapi di sistem keuangan selisih sekecil apa pun itu masalah. Total invoice bisa tidak cocok dengan rincian, dan pembulatan yang menumpuk bikin laporan akuntansi tidak balance. Solusinya DECIMAL, yang menyimpan angka secara eksak sesuai digit yang kamu tentukan:

    CREATE TABLE demo_decimal (saldo DECIMAL(15,2));
    INSERT INTO demo_decimal VALUES (0.1), (0.2);
    SELECT SUM(saldo) FROM demo_decimal;
    +------------+
    | SUM(saldo) |
    +------------+
    |       0.30 |
    +------------+

    DECIMAL(15,2) artinya total 15 digit, 2 di antaranya di belakang koma. Untuk rupiah, DECIMAL(15,2) sudah menampung sampai ratusan triliun. Di proyek klien yang tim Arrazy kerjakan, semua kolom harga, saldo, dan total transaksi pada sistem aplikasi yang kami bangun selalu memakai DECIMAL, tanpa pengecualian. FLOAT dan DOUBLE hanya layak untuk data ilmiah atau pengukuran yang memang toleran terhadap pendekatan, misalnya koordinat atau hasil sensor.

    Tipe Data Teks: VARCHAR, CHAR, dan TEXT

    Tiga tipe ini sering ketukar. Bedanya ada di cara penyimpanan:

    • VARCHAR(n) menyimpan teks dengan panjang bervariasi sampai maksimal n karakter. Kata “Budi” di kolom VARCHAR(100) hanya memakai ruang sebesar 4 karakter plus 1 byte penanda panjang. Ini pilihan default untuk hampir semua teks: nama, email, judul, alamat.
    • CHAR(n) selalu memakai ruang tetap n karakter, sisa ruangnya diisi spasi. Cocok hanya untuk data yang panjangnya benar-benar seragam, misalnya kode provinsi 2 huruf atau kode mata uang seperti IDR dan USD.
    • TEXT untuk teks panjang yang tidak jelas batasnya, misalnya isi artikel atau deskripsi produk. TEXT disimpan terpisah dari baris utama, tidak bisa punya nilai DEFAULT, dan kalau mau diindex harus pakai prefix index. Jadi jangan pakai TEXT untuk kolom yang sebenarnya pendek.

    Soal memilih panjang VARCHAR, pakai angka yang wajar sesuai data aslinya. VARCHAR(100) untuk nama orang, VARCHAR(255) untuk email atau URL, VARCHAR(20) untuk nomor telepon. Nomor telepon disimpan sebagai teks, bukan angka, karena ada nol di depan dan kadang tanda plus.

    Angka panjang di VARCHAR tidak bikin baris lebih besar selama isinya pendek, tapi tetap jangan asal tulis VARCHAR(5000). MySQL memakai panjang deklarasi saat membuat tabel sementara di memori untuk operasi tertentu, dan batas satu baris InnoDB juga terbatas sekitar 65.535 byte untuk semua kolom non TEXT. Deklarasi yang jujur juga berfungsi sebagai validasi gratis: kolom kode pos VARCHAR(10) otomatis menolak input yang jelas salah.

    CREATE TABLE pelanggan (
      id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
      nama VARCHAR(100) NOT NULL,
      email VARCHAR(255) NOT NULL,
      telepon VARCHAR(20),
      kode_negara CHAR(2) NOT NULL DEFAULT 'ID',
      catatan TEXT
    );

    Tipe Data Tanggal dan Waktu: DATE, DATETIME, TIMESTAMP

    Tiga tipe utama untuk waktu, dengan fungsi berbeda:

    • DATE hanya tanggal, format YYYY-MM-DD. Pas untuk tanggal lahir atau tanggal jatuh tempo.
    • DATETIME tanggal plus jam, rentang tahun 1000 sampai 9999. Nilainya disimpan apa adanya, tidak peduli timezone server.
    • TIMESTAMP tanggal plus jam juga, tapi rentangnya terbatas dari tahun 1970 sampai awal 2038. Nilainya dikonversi ke UTC saat disimpan, lalu dikonversi balik ke timezone sesi saat dibaca.

    Perbedaan Perilaku Timezone DATETIME vs TIMESTAMP

    Ini perbedaan paling penting dan paling sering bikin bingung. Jalankan percobaan ini:

    CREATE TABLE demo_waktu (
      pakai_datetime DATETIME,
      pakai_timestamp TIMESTAMP
    );
    
    SET time_zone = '+07:00';
    INSERT INTO demo_waktu VALUES ('2026-07-27 10:00:00', '2026-07-27 10:00:00');
    
    SET time_zone = '+00:00';
    SELECT * FROM demo_waktu;

    Hasilnya:

    +---------------------+---------------------+
    | pakai_datetime      | pakai_timestamp     |
    +---------------------+---------------------+
    | 2026-07-27 10:00:00 | 2026-07-27 03:00:00 |
    +---------------------+---------------------+

    Kolom DATETIME tetap menunjukkan jam 10 pagi karena nilainya disimpan mentah. Kolom TIMESTAMP bergeser jadi jam 3 karena MySQL menyimpannya sebagai UTC lalu menampilkannya sesuai timezone sesi yang sekarang kita ubah ke +00:00. Perilaku ini berguna kalau aplikasimu punya pengguna lintas timezone, tapi bisa mengejutkan kalau kamu tidak sadar.

    Rekomendasi praktis untuk aplikasi Indonesia yang penggunanya satu timezone: pakai DATETIME untuk data bisnis seperti jadwal dan tanggal transaksi, lalu pakai TIMESTAMP untuk kolom audit created_at dan updated_at. Untuk kolom audit, MySQL bisa mengisinya otomatis:

    CREATE TABLE pesanan (
      id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
      total DECIMAL(15,2) NOT NULL,
      tanggal_kirim DATE,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    );

    Satu catatan lagi: TIMESTAMP mentok di 19 Januari 2038. Untuk kolom yang mungkin berisi tanggal jauh di masa depan, misalnya masa berlaku kontrak, pakai DATETIME.

    ENUM, BOOLEAN, dan Kapan Pakai Tabel Referensi

    ENUM membatasi isi kolom pada daftar nilai yang kamu tetapkan saat membuat tabel:

    CREATE TABLE pesanan_status (
      id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
      status ENUM('menunggu', 'dibayar', 'dikirim', 'selesai') NOT NULL DEFAULT 'menunggu'
    );

    Nilai di luar daftar akan ditolak, jadi datamu terjaga. Kelemahannya, menambah status baru berarti harus ALTER TABLE, dan itu operasi yang berat di tabel besar. ENUM juga diurutkan berdasarkan posisi di daftar, bukan alfabet, yang kadang bikin ORDER BY terasa aneh.

    MySQL tidak punya tipe BOOLEAN sungguhan. Saat kamu menulis BOOLEAN atau BOOL, MySQL diam-diam membuatnya sebagai TINYINT(1), dengan 0 dianggap false dan selain 0 dianggap true:

    CREATE TABLE pengguna (
      id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
      aktif BOOLEAN NOT NULL DEFAULT TRUE
    );
    
    SHOW CREATE TABLE pengguna\G

    Di output SHOW CREATE TABLE kamu akan melihat kolom aktif tertulis sebagai tinyint(1). Tidak masalah, itu memang cara MySQL. Yang penting konsisten: simpan hanya 0 dan 1.

    Lalu kapan sebaiknya pakai tabel referensi ketimbang ENUM? Pakai tabel referensi kalau daftarnya berpotensi bertambah lewat aplikasi, perlu menyimpan info tambahan per nilai, atau perlu diubah admin tanpa menyentuh struktur database. Contoh: kategori produk jelas lebih cocok jadi tabel sendiri yang nanti dihubungkan lewat foreign key, materi yang kita bahas tuntas di bagian 11 seri ini. ENUM cukup untuk daftar yang benar-benar stabil seperti status pesanan atau jenis kelamin.

    Troubleshooting Error Tipe Data yang Sering Muncul

    MySQL 8.4 berjalan dalam strict mode secara default, jadi data yang tidak cocok dengan tipe kolom langsung ditolak dengan error, bukan dipotong diam-diam. Ini bagus, tapi berarti kamu akan sering ketemu error berikut saat belajar.

    ERROR 1264: Out of range value for column

    INSERT INTO produk (nama, stok) VALUES ('Tes', 70000);
    ERROR 1264 (22003): Out of range value for column 'stok' at row 1

    Penyebab: nilai melebihi kapasitas tipe. Di contoh ini stok bertipe SMALLINT UNSIGNED yang maksimal 65.535. Solusinya perbesar tipenya, misalnya ALTER TABLE produk MODIFY stok INT UNSIGNED NOT NULL DEFAULT 0;. Error yang sama muncul kalau kamu memasukkan angka negatif ke kolom UNSIGNED.

    ERROR 1265: Data truncated for column

    INSERT INTO pesanan_status (status) VALUES ('batal');
    ERROR 1265 (01000): Data truncated for column 'status' at row 1

    Penyebab paling umum: memasukkan nilai yang tidak ada di daftar ENUM, atau memasukkan teks ke kolom angka. Cek daftar nilai yang sah dengan SHOW COLUMNS FROM pesanan_status; lalu perbaiki nilainya, atau tambahkan nilai baru ke ENUM lewat ALTER TABLE kalau memang dibutuhkan.

    ERROR 1406: Data too long for column

    INSERT INTO pelanggan (nama, email, kode_negara)
    VALUES ('Budi', 'budi@contoh.com', 'IDN');
    ERROR 1406 (22001): Data too long for column 'kode_negara' at row 1

    Penyebab: teks lebih panjang dari deklarasi kolom, di sini CHAR(2) diisi 3 huruf. Kalau datanya yang salah, perbaiki datanya. Kalau deklarasinya yang terlalu sempit, perlebar dengan ALTER TABLE ... MODIFY.

    ERROR 1292: Incorrect datetime value

    INSERT INTO pesanan (total, tanggal_kirim) VALUES (150000, '27-07-2026');
    ERROR 1292 (22007): Incorrect date value: '27-07-2026' for column 'tanggal_kirim' at row 1

    Penyebab: format tanggal salah. MySQL menerima format YYYY-MM-DD, bukan DD-MM-YYYY gaya Indonesia. Tulis '2026-07-27'. Kalau sumber datanya memang berformat lain, konversi dulu dengan fungsi STR_TO_DATE('27-07-2026', '%d-%m-%Y').

    Rangkuman dan Lanjut ke Bagian 5

    Pegangan singkatnya: INT untuk bilangan bulat dan naikkan ke BIGINT hanya kalau perlu, DECIMAL untuk semua nilai uang, VARCHAR dengan panjang wajar untuk teks pendek dan TEXT untuk konten panjang, DATETIME untuk waktu bisnis dan TIMESTAMP untuk kolom audit, lalu ENUM hanya untuk daftar yang stabil. Tipe yang tepat sejak awal jauh lebih murah daripada ALTER TABLE di tabel yang sudah berisi jutaan baris.

    Di bagian berikutnya, Belajar Database dari Nol #5: INSERT, Menambah Data ke Tabel, kita mulai mengisi tabel dengan berbagai variasi perintah INSERT, termasuk memasukkan banyak baris sekaligus. Artikelnya terbit menyusul dan bisa kamu pantau di halaman hub seri Belajar Database.

    Referensi

  • Biaya Bulanan Menjalankan Website: Yang Perlu Dianggarkan

    Biaya Bulanan Menjalankan Website: Yang Perlu Dianggarkan

    Website sudah jadi bukan berarti urusan biaya selesai. Ada beberapa pos yang tetap harus dibayar supaya website tetap online. Komponennya: perpanjangan domain (dibayar tahunan), hosting (bulanan atau tahunan), SSL (sering sudah gratis, tapi ada kasus tertentu yang berbayar), email bisnis (opsional), maintenance (opsional tapi disarankan), dan biaya konten kalau Anda menyerahkan penulisannya ke pihak lain.

    Sebagai gambaran kasar, website bisnis sederhana seperti company profile biasanya menghabiskan sekitar Rp 1 juta sampai Rp 2 juta per tahun untuk domain dan hosting saja. Kalau ditambah email bisnis dan maintenance rutin, angkanya bisa naik ke kisaran Rp 8 juta sampai Rp 12 juta per tahun. Angka ini bisa berbeda di tiap penyedia, jadi anggap sebagai patokan awal, bukan harga pasti. Artikel ini membedah tiap komponennya satu per satu supaya Anda tidak kaget saat tagihan tahun kedua datang.

    Domain dan hosting, dua biaya yang tidak bisa dihindari

    Dua pos ini wajib. Tanpa domain, alamat website Anda hilang. Tanpa hosting, file website tidak punya tempat tinggal. Kalau masih bingung bedanya, kami pernah menulis penjelasannya di artikel beda domain dan hosting untuk pemilik bisnis.

    Perpanjangan domain: kecil tapi paling berisiko kalau telat

    Domain dibayar per tahun. Untuk pasar Indonesia, domain .com umumnya sekitar Rp 150 ribu sampai Rp 250 ribu per tahun. Domain .id biasanya sedikit lebih mahal, sekitar Rp 200 ribu sampai Rp 350 ribu per tahun. Tiap registrar punya harga sendiri, jadi cek langsung sebelum menganggarkan.

    Satu hal yang sering terlewat: harga promo tahun pertama hampir selalu lebih murah dari harga perpanjangan. Domain yang Anda beli Rp 100 ribu bisa jadi diperpanjang di harga Rp 200 ribu lebih. Itu normal, bukan penipuan. Yang penting Anda tahu sejak awal.

    Risiko terbesar dari domain bukan harganya, tapi telat bayar. Domain yang lewat masa aktif akan membuat website dan email langsung mati. Lewat masa tenggang, domain bisa masuk periode penebusan dengan biaya berkali lipat. Kalau sampai dilepas, orang lain bisa membelinya. Pasang pengingat, atau aktifkan perpanjangan otomatis.

    Hosting: shared cukup untuk company profile, VPS untuk sistem

    Untuk website company profile atau landing page, shared hosting biasanya sudah cukup. Kisarannya sekitar Rp 20 ribu sampai Rp 100 ribu per bulan, tergantung kapasitas dan penyedia. Banyak yang menawarkan pembayaran tahunan dengan harga lebih hemat.

    VPS baru diperlukan kalau website Anda menjalankan sistem, misalnya aplikasi internal, toko online dengan transaksi ramai, atau portal dengan ribuan pengunjung per hari. Biayanya mulai sekitar Rp 100 ribu sampai Rp 500 ribu lebih per bulan, tergantung spesifikasi.

    Kapan waktunya naik kelas dari shared ke VPS? Ada beberapa tanda yang mudah dikenali. Website mulai lambat di jam ramai. Hosting sering mengirim peringatan resource habis. Atau Anda mulai menjalankan fitur yang butuh proses berat seperti laporan otomatis dan integrasi API. Kalau salah satu tanda itu muncul rutin, saatnya diskusi upgrade.

    Soal SSL, kabar baiknya sertifikat dasar sekarang umumnya gratis. Kebanyakan hosting sudah menyertakan Let’s Encrypt tanpa biaya tambahan. SSL berbayar baru relevan kalau Anda butuh validasi organisasi atau garansi tertentu, misalnya untuk lembaga keuangan. Untuk mayoritas website bisnis, yang gratis sudah aman.

    Email bisnis dan maintenance, opsional tapi layak dihitung

    Email dengan nama domain sendiri, misalnya halo@namabisnis.com, tidak wajib. Tapi untuk bisnis yang sering berkirim penawaran, email berdomain jauh lebih dipercaya daripada alamat gmail biasa. Ada dua jalur umum. Pertama, email hosting bawaan paket hosting, sering sudah termasuk tanpa biaya ekstra tapi kapasitas dan deliverability-nya terbatas. Kedua, layanan seperti Google Workspace yang berbayar sekitar seratus ribuan per pengguna per bulan. Pilih sesuai kebutuhan, bukan sesuai gengsi.

    Maintenance juga opsional, tapi kami menyarankannya untuk website yang jadi ujung tombak bisnis. Cakupannya biasanya update sistem dan plugin, backup rutin, pemantauan uptime, dan perbaikan kecil. Di pasar Indonesia, paket maintenance website umumnya mulai sekitar Rp 300 ribu sampai Rp 1 juta lebih per bulan, tergantung cakupan. Website yang tidak pernah diupdate lama kelamaan jadi sasaran empuk celah keamanan, dan biaya membersihkan website yang diretas hampir selalu lebih mahal daripada biaya merawatnya.

    Satu pos lagi yang sering lupa dianggarkan: konten. Kalau Anda menulis artikel dan mengelola halaman sendiri, biayanya nol rupiah tapi bayar pakai waktu. Kalau di-outsource ke penulis atau agensi, hitung sebagai biaya rutin bulanan juga.

    Biaya yang sering bikin kaget di tahun kedua

    Kebanyakan pemilik website tidak kaget di tahun pertama. Kagetnya di tahun kedua. Ini tiga penyebab paling umum.

    • Harga renewal vs harga promo. Hosting yang dipromosikan Rp 30 ribu per bulan bisa diperpanjang di harga dua sampai tiga kali lipat. Promo hanya berlaku periode pertama. Selalu cek harga perpanjangan sebelum membeli, biasanya tertera di halaman order.
    • Lisensi plugin dan template premium. Banyak website WordPress memakai theme atau plugin berbayar. Lisensinya sering tahunan. Kalau tidak diperpanjang, website biasanya tetap jalan, tapi Anda kehilangan update keamanan dan dukungan. Tanyakan ke pembuat website Anda: plugin apa saja yang berbayar, dan siapa yang menanggung perpanjangannya.
    • Backup yang ternyata berbayar terpisah. Tidak semua paket hosting menyertakan backup otomatis. Ada yang menjualnya sebagai layanan tambahan. Jangan berasumsi data Anda aman hanya karena hostingnya jalan. Pastikan ada backup rutin, entah dari hosting, dari plugin, atau dari tim maintenance.

    Simulasi anggaran tahunan: dua skenario

    Supaya lebih kebayang, ini dua simulasi dengan angka bulat yang konservatif. Sekali lagi, ini gambaran, bukan penawaran harga. Angka aslinya tergantung penyedia dan kebutuhan Anda.

    Skenario 1: company profile hemat

    • Perpanjangan domain .com: Rp 250 ribu per tahun
    • Shared hosting: Rp 50 ribu per bulan, sekitar Rp 600 ribu per tahun
    • SSL: gratis dari hosting
    • Email: pakai email bawaan hosting, tanpa biaya tambahan
    • Maintenance: dikelola sendiri

    Total sekitar Rp 850 ribu sampai Rp 1 juta per tahun. Cocok untuk bisnis yang websitenya berfungsi sebagai kartu nama online dan jarang berubah.

    Skenario 2: website bisnis dengan email dan maintenance

    • Perpanjangan domain: Rp 250 ribu per tahun
    • Hosting kelas menengah: Rp 150 ribu per bulan, sekitar Rp 1,8 juta per tahun
    • Email bisnis 2 pengguna: sekitar Rp 2,4 juta per tahun
    • Maintenance bulanan: Rp 500 ribu per bulan, Rp 6 juta per tahun

    Total sekitar Rp 10 juta per tahun, atau kurang lebih Rp 850 ribu per bulan. Terdengar besar kalau dilihat setahun, tapi sebanding untuk website yang aktif mendatangkan leads dan jadi kanal utama calon pelanggan mengenal bisnis Anda.

    Hemat yang sehat vs hemat yang bahaya

    Menekan biaya itu wajar. Yang penting tahu mana penghematan yang aman dan mana yang menanam masalah.

    Hemat yang sehat: bayar hosting dan domain sekaligus setahun di depan, biasanya lebih murah daripada bulanan. Pakai shared hosting selama trafik masih kecil. Pakai SSL gratis. Tulis konten sendiri kalau memang sanggup konsisten.

    Hemat yang bahaya bentuknya lain. Pindah ke hosting abal-abal yang super murah tapi sering down dan supportnya hilang saat dibutuhkan. Melewatkan backup karena merasa tidak akan kenapa-kenapa. Atau yang paling sering kami temui: domain dan hosting terdaftar atas nama vendor, bukan atas nama Anda. Kalau suatu hari hubungan dengan vendor berakhir buruk, Anda bisa kehilangan akses ke aset digital sendiri. Pastikan kepemilikan domain ada di nama bisnis Anda, atau minimal Anda pegang akses penuh ke akun registrarnya.

    Tiga pertanyaan wajib sebelum bayar ke vendor

    Sebelum menandatangani penawaran pembuatan website, ajukan tiga pertanyaan ini. Jawabannya menentukan berapa biaya rutin Anda ke depan.

    • Tahun pertama sudah termasuk apa saja? Domain, hosting, SSL, email, berapa lama masa aktifnya, dan apakah ada lisensi plugin yang ikut ditanggung.
    • Tahun kedua saya bayar apa, dan berapa? Minta rinciannya tertulis. Vendor yang jujur tidak akan keberatan menjawab ini.
    • Siapa yang pegang akses? Akun domain, hosting, dan admin website sebaiknya bisa Anda akses sendiri. Kalau semua akses hanya di tangan vendor, Anda sedang menyewa, bukan memiliki.

    Di Arrazy Inovasi, kami terbiasa merinci biaya tahun pertama dan tahun berikutnya sejak awal penawaran, termasuk serah terima akses ke klien. Kalau Anda sedang menimbang membuat website atau ingin membenahi website yang sudah ada, lihat layanan kami di halaman jasa pembuatan website atau langsung hubungi kami untuk diskusi kebutuhan Anda. Konsultasi awal tidak dipungut biaya.

    Pertanyaan yang sering diajukan

    Kalau saya tidak bayar apa pun setelah website jadi, apa yang terjadi?

    Website tetap jalan sampai masa aktif domain dan hosting habis. Setelah itu website mati, email berdomain ikut mati, dan kalau domain lewat masa tenggang, biayanya menebus jauh lebih mahal atau domain hilang diambil orang lain.

    Apakah maintenance benar-benar perlu untuk website kecil?

    Untuk company profile sederhana yang jarang berubah, Anda bisa mengelolanya sendiri asal rajin update dan punya backup. Maintenance berbayar baru terasa nilainya saat website jadi sumber leads utama, sering diubah, atau memakai banyak plugin yang harus dijaga tetap aman.

    Lebih hemat bayar hosting bulanan atau tahunan?

    Hampir selalu lebih hemat tahunan. Selisihnya bisa setara satu sampai dua bulan gratis. Bayar bulanan masuk akal hanya kalau Anda masih mencoba penyedia baru dan belum yakin dengan kualitasnya.

  • Cara Membuat Email Bisnis dengan Nama Domain Sendiri

    Cara Membuat Email Bisnis dengan Nama Domain Sendiri

    Bayangkan Anda menerima penawaran kerja sama dari dua vendor. Satu mengirim dari alamat seperti tokoberkahjaya88@gmail.com. Satu lagi dari halo@berkahjaya.com. Isi penawarannya mirip. Tapi yang kedua terasa lebih serius, kan. Padahal bedanya cuma alamat email.

    Banyak pemilik usaha masih menjalankan bisnisnya dari email Gmail pribadi. Wajar, karena gratis dan sudah terbiasa. Masalahnya, begitu usaha mulai berhadapan dengan klien perusahaan, instansi, atau supplier besar, alamat email pribadi sering jadi ganjalan pertama. Di artikel sebelumnya tentang beda domain dan hosting, kami sempat menyinggung soal email profesional satu kalimat saja. Artikel ini kelanjutannya. Kita bahas tuntas cara membuat email bisnis dengan nama domain sendiri, dalam bahasa yang bisa dipahami tanpa latar belakang IT.

    Apa yang Sebenarnya Dibutuhkan untuk Punya Email @NamaUsaha

    Jawabannya sederhana. Anda butuh dua hal: domain sendiri dan layanan email yang dihubungkan ke domain itu.

    Domain adalah alamat usaha Anda di internet, misalnya usahamu.com. Kalau sudah punya website, berarti Anda sudah punya domain. Kalau belum, domain bisa dibeli terpisah mulai dari sekitar 150 ribu sampai 250 ribu rupiah per tahun untuk akhiran .com.

    Nah, domain itu sendiri belum bisa menerima email. Ia perlu dihubungkan ke layanan email. Ada tiga jalur umum yang bisa dipilih:

    • Email bawaan hosting. Kalau website Anda sudah pakai hosting, biasanya paket hosting sudah termasuk fitur email. Artinya Anda bisa bikin alamat nama@usahamu.com tanpa biaya tambahan.
    • Layanan email profesional per user. Ini kategori layanan seperti Google Workspace atau Microsoft 365. Anda bayar biaya bulanan per alamat email, dan sebagai gantinya dapat kapasitas besar, aplikasi yang familiar, dan keandalan tinggi.
    • Email forwarding sederhana. Alamat halo@usahamu.com dibuat hanya sebagai penerus. Email yang masuk ke sana diteruskan otomatis ke Gmail pribadi Anda. Murah, kadang gratis, cocok untuk tahap awal.

    Ketiganya sama-sama menghasilkan alamat email dengan nama usaha Anda. Bedanya di biaya, kenyamanan, dan seberapa siap untuk berkembang.

    Kenapa Repot-repot, Padahal Gmail Gratis

    Pertanyaan yang masuk akal. Ini alasan kenapa email bisnis layak diurus, terutama kalau usaha Anda mulai sering berhubungan dengan pihak luar.

    Kesan pertama saat kirim penawaran. Penerima menilai email sebelum membacanya. Alamat dengan nama usaha memberi sinyal bahwa bisnis ini nyata, punya identitas, dan berniat jangka panjang. Untuk tender atau kerja sama dengan perusahaan besar, ini sering jadi syarat tidak tertulis.

    Kepercayaan pelanggan. Email konfirmasi pesanan dari cs@usahamu.com terasa lebih aman dibanding dari alamat Gmail acak. Di masa marak penipuan online, hal kecil seperti ini menenangkan pembeli.

    Kontrol saat karyawan keluar. Ini yang paling sering dilupakan. Kalau karyawan melayani pelanggan lewat Gmail pribadinya, saat dia resign, seluruh riwayat percakapan ikut pergi. Dengan email bisnis, akun bisa Anda nonaktifkan atau alihkan ke penggantinya. Pelanggan tetap terlayani tanpa terputus.

    Riwayat terpusat. Semua komunikasi bisnis ada di satu sistem yang Anda kuasai. Mau cari penawaran dua tahun lalu atau bukti kesepakatan harga, tinggal cari di satu tempat.

    Perbandingan Jujur Tiga Jalur Email Bisnis

    Tidak ada satu jawaban yang benar untuk semua usaha. Berikut kelebihan dan kekurangan masing-masing jalur apa adanya.

    Email bawaan hosting

    Kelebihannya jelas: sering kali tidak ada biaya tambahan karena sudah termasuk paket hosting, jumlah alamat bisa banyak, dan cukup untuk kebutuhan komunikasi standar. Kekurangannya, kapasitas penyimpanan ikut berbagi dengan file website, tampilan webmail bawaannya terasa jadul, dan kalau server hosting bermasalah, email ikut terganggu. Untuk pengiriman massal, email hosting juga lebih rawan masuk folder spam penerima.

    Layanan profesional per user

    Kelebihannya: kapasitas besar dan terpisah dari hosting, tampilan aplikasi yang sudah dikenal semua orang, keandalan tinggi, dan email jarang nyasar ke spam. Cocok kalau tim Anda sudah lebih dari beberapa orang dan email jadi urat nadi komunikasi. Kekurangannya satu, biaya. Anda bayar per alamat per bulan. Untuk tim 5 orang, biayanya bisa ratusan ribu rupiah per bulan. Buat usaha yang emailnya cuma dipakai sesekali, ini terasa berlebihan.

    Forwarding sederhana

    Kelebihannya: paling hemat, kadang gratis dari registrar domain, dan Anda tetap kerja di Gmail yang sudah nyaman. Alamat halo@usahamu.com tampil profesional ke pelanggan, padahal di belakang layar masuk ke inbox lama Anda. Kekurangannya, jalur ini pada dasarnya satu arah. Menerima gampang, tapi untuk membalas sebagai halo@usahamu.com perlu pengaturan tambahan yang kadang ribet. Riwayat juga tetap tercampur dengan email pribadi, jadi masalah kontrol karyawan tadi tidak terselesaikan.

    Ringkasnya begini. Baru mulai dan anggaran ketat, forwarding atau email hosting sudah cukup. Sudah punya website dan hosting, pakai dulu email bawaan hostingnya, itu yang paling masuk akal. Tim mulai besar dan email jadi alat kerja utama, baru pertimbangkan layanan per user.

    Langkah Setup Secara Garis Besar

    Anda tidak perlu paham detail teknisnya, tapi ada baiknya tahu gambaran besarnya supaya bisa berkomunikasi dengan vendor atau developer.

    Jalur email hosting. Masuk ke panel hosting (biasanya cPanel), cari menu Email Accounts, buat alamat baru, selesai. Kalau domain dan hosting dibeli di tempat yang sama, biasanya tidak ada pengaturan tambahan.

    Jalur layanan profesional. Daftar ke penyedia layanan, verifikasi bahwa domain itu milik Anda, lalu atur MX record di pengaturan domain. MX record itu semacam penunjuk arah yang memberi tahu ke mana surat untuk domain Anda harus diantarkan. Setelah itu buat akun untuk tiap anggota tim.

    Jalur forwarding. Aktifkan fitur email forwarding di tempat Anda membeli domain, tentukan alamat penerusnya, selesai dalam hitungan menit.

    Jujur saja, bagian mengubah MX record dan pengaturan domain ini biasanya bukan pekerjaan pemilik usaha. Developer website atau tim support hosting Anda bisa menyelesaikannya dalam hitungan menit. Jadi jangan sampai istilah teknis ini membuat Anda menunda. Tugas Anda cukup memutuskan jalur mana yang dipakai dan siapa saja yang butuh alamat email.

    Penamaan Alamat dan Kebiasaan Baik Setelah Email Aktif

    Supaya rapi sejak awal, pakai pola penamaan yang konsisten:

    • Alamat personal pakai nama orangnya: budi@usahamu.com atau budi.santoso@usahamu.com kalau timnya besar.
    • Alamat umum pakai fungsi: halo@ untuk kontak umum, cs@ untuk layanan pelanggan, admin@ atau finance@ untuk keperluan internal.

    Alamat fungsi seperti cs@ penting karena tidak menempel ke satu orang. Karyawan boleh berganti, alamatnya tetap sama di mata pelanggan.

    Beberapa kebiasaan baik yang sebaiknya dijalankan sejak hari pertama:

    • Password kuat dan tidak dipakai ulang. Email bisnis berisi data pelanggan dan kesepakatan harga. Jangan pakai password yang sama dengan akun lain.
    • Pasang di HP. Email bisnis yang hanya dicek dari laptop kantor akan lambat merespons. Hubungkan ke aplikasi email di ponsel supaya balasan tetap cepat.
    • Buat tanda tangan email. Nama, jabatan, nama usaha, nomor telepon, dan alamat website. Setiap email yang keluar jadi sekaligus kartu nama.

    Ada dua kesalahan umum yang sering merugikan. Pertama, lupa perpanjang domain. Kalau domain expired, website mati dan email ikut mati, semua pesan yang dikirim orang ke Anda akan mental. Aktifkan perpanjangan otomatis, dan pahami kaitannya di artikel beda domain dan hosting tadi. Kedua, akun email atau bahkan akun domain dibuat pakai data pribadi karyawan. Saat karyawan itu keluar, Anda kehilangan akses ke aset sendiri. Pastikan semua akun terdaftar atas nama usaha dan Anda memegang akses utamanya.

    Pertanyaan yang Sering Muncul

    Apakah email bisnis bisa dibuka di aplikasi Gmail di HP?

    Bisa. Aplikasi Gmail di ponsel bisa menampung akun email dari hosting maupun layanan lain, tinggal tambahkan akun baru di pengaturannya. Jadi Anda tidak perlu belajar aplikasi baru. Yang berubah hanya alamatnya, kebiasaan kerjanya tetap sama.

    Berapa biaya yang perlu disiapkan?

    Kalau sudah punya website dengan hosting, sering kali nol rupiah karena fitur email sudah termasuk. Kalau mulai dari nol, siapkan biaya domain sekitar 150 sampai 250 ribu rupiah per tahun. Layanan profesional per user umumnya mulai dari kisaran puluhan ribu rupiah per alamat per bulan, tergantung paket dan penyedia.

    Email Gmail lama saya bagaimana?

    Tidak perlu dihapus. Biarkan aktif untuk urusan pribadi, dan arahkan komunikasi bisnis baru ke alamat domain. Untuk masa transisi, pasang balasan otomatis atau penerusan dari Gmail lama ke email bisnis, lalu kabari pelanggan tetap Anda soal alamat baru itu.

    Terakhir, soal mulai dari mana. Kalau website Anda sudah jalan, langkah tercepatnya cek dulu ke penyedia hosting, kemungkinan besar fitur emailnya sudah ada dan tinggal diaktifkan. Kalau belum punya domain sama sekali, urutan paling hemat adalah beli domain dulu, lalu pilih salah satu dari tiga jalur di atas sesuai kebutuhan dan anggaran.

    Dan kalau Anda memang sedang berencana membuat website untuk usaha, sekalian saja. Di paket jasa website Arrazy, penyiapan email bisnis dengan domain sendiri bisa dikerjakan bersamaan, jadi Anda tidak perlu pusing dengan MX record dan kawan-kawannya. Silakan hubungi kami untuk ngobrol dulu soal kebutuhan usaha Anda, tanpa kewajiban apa pun.

  • Belajar Golang dari Nol #22: HTTP Client ke API Pihak Ketiga

    Belajar Golang dari Nol #22: HTTP Client ke API Pihak Ketiga

    Di Belajar Golang dari Nol #21 kita belajar mengirim email dari aplikasi Go, lengkap dengan retry saat SMTP bermasalah. Sekarang arah pandangnya kita balik. Sejak bagian 11 aplikasi kita selalu jadi server, pihak yang dipanggil. Kali ini kita jadi pihak yang memanggil. Aplikasi kita akan bertamu ke API milik orang lain. Kalau kamu baru bergabung di seri ini, mampir dulu ke daftar lengkap seri supaya urutannya jelas.

    Kenapa aplikasimu hampir pasti butuh HTTP client

    Hampir tidak ada aplikasi bisnis yang hidup sendirian. Toko online perlu cek ongkir ke API kurir sebelum checkout. Sistem kasir perlu memotong pembayaran lewat payment gateway seperti Midtrans atau Xendit. Aplikasi PPDB perlu kirim notifikasi lewat WhatsApp gateway. Dashboard keuangan perlu ambil kurs dollar hari ini.

    Semua kebutuhan itu jalannya sama: aplikasimu mengirim HTTP request ke server orang lain, lalu membaca balasannya. Alatnya sudah ada di paket net/http yang kita pakai sejak membangun REST API. Bedanya, sekarang kita memakai sisi client-nya.

    Jangan pakai http.Get polos di production

    Cara tercepat memanggil API memang satu baris:

    resp, err := http.Get("https://api.contoh.com/data")

    Kode ini jalan, dan justru itu bahayanya. http.Get memakai http.DefaultClient, dan DefaultClient tidak punya timeout sama sekali. Kalau server tujuan lambat atau koneksinya menggantung, request-mu ikut menggantung. Bukan sebentar. Selamanya.

    Di bagian 17 kita sudah bahas kenapa timeout itu nyawa. Setiap request yang menggantung menahan satu goroutine, satu koneksi, dan kadang satu user yang menunggu di depan layar. Kalau API pihak ketiga sedang down dan kamu tidak pasang timeout, antrean request menumpuk sampai aplikasimu ikut tumbang. Padahal yang rusak bukan aplikasimu.

    Solusinya sederhana: buat http.Client sendiri dengan timeout eksplisit, lalu pakai ulang di seluruh aplikasi.

    var httpClient = &http.Client{
    	Timeout: 10 * time.Second,
    }

    Satu client ini aman dipakai dari banyak goroutine sekaligus. Jangan membuat client baru tiap request, karena client menyimpan pool koneksi yang bisa dipakai ulang.

    GET yang benar: context, Body.Close, cek status

    Begini pola lengkap request GET yang layak masuk production:

    func ambilTodo(ctx context.Context) ([]byte, error) {
    	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    	defer cancel()
    
    	req, err := http.NewRequestWithContext(ctx, http.MethodGet,
    		"https://jsonplaceholder.typicode.com/todos/1", nil)
    	if err != nil {
    		return nil, err
    	}
    
    	resp, err := httpClient.Do(req)
    	if err != nil {
    		return nil, fmt.Errorf("request gagal: %w", err)
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode != http.StatusOK {
    		return nil, fmt.Errorf("status tidak terduga: %d", resp.StatusCode)
    	}
    
    	return io.ReadAll(resp.Body)
    }

    Ada tiga kebiasaan penting di sini.

    Pertama, http.NewRequestWithContext. Request ini membawa context, jadi kalau user membatalkan halaman atau deadline lewat, request ke pihak ketiga ikut dibatalkan. Ini sambungan langsung dari materi context di bagian 17.

    Kedua, defer resp.Body.Close(). Ini wajib, bukan hiasan. Selama body belum ditutup, koneksi TCP di baliknya tidak bisa dikembalikan ke pool. Lupa menutup body di satu fungsi yang dipanggil ribuan kali berarti ribuan koneksi bocor. Gejalanya pelan: aplikasi makin lama makin berat, file descriptor habis, lalu error aneh muncul di jam sibuk. Tulis defer resp.Body.Close() tepat setelah cek error, sebelum kode lain.

    Ketiga, cek resp.StatusCode sebelum parse. Server yang membalas error 500 sering mengirim halaman HTML, bukan JSON. Kalau kamu langsung decode tanpa cek status, error yang muncul adalah “invalid character” yang membingungkan, padahal masalah aslinya server sedang rusak.

    Decode JSON respons ke struct

    Untuk latihan, pakai API publik yang stabil seperti jsonplaceholder.typicode.com atau httpbin.org. Keduanya gratis dan memang disediakan untuk uji coba. Endpoint /todos/1 di jsonplaceholder membalas JSON seperti ini:

    {
      "userId": 1,
      "id": 1,
      "title": "delectus aut autem",
      "completed": false
    }

    Kita tampung ke struct dengan json tag, persis seperti saat kita menerima request body di bagian 11, hanya arahnya terbalik:

    type Todo struct {
    	UserID    int    `json:"userId"`
    	ID        int    `json:"id"`
    	Title     string `json:"title"`
    	Completed bool   `json:"completed"`
    }
    
    func main() {
    	body, err := ambilTodo(context.Background())
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	var todo Todo
    	if err := json.Unmarshal(body, &todo); err != nil {
    		log.Fatal(err)
    	}
    	fmt.Printf("judul: %s, selesai: %t\n", todo.Title, todo.Completed)
    }

    Field yang tidak ada di struct akan diabaikan, jadi kamu cukup mendeklarasikan field yang benar-benar kamu pakai. Respons API pihak ketiga sering gemuk, ambil yang perlu saja.

    POST JSON: kirim data plus kunci API

    Memanggil payment gateway atau WhatsApp gateway hampir selalu berupa POST dengan body JSON dan header autentikasi. Polanya begini:

    type PermintaanBayar struct {
    	OrderID string `json:"order_id"`
    	Jumlah  int64  `json:"jumlah"`
    	Metode  string `json:"metode"`
    }
    
    func buatTagihan(ctx context.Context) error {
    	payload, err := json.Marshal(PermintaanBayar{
    		OrderID: "INV-2026-001",
    		Jumlah:  150000,
    		Metode:  "qris",
    	})
    	if err != nil {
    		return err
    	}
    
    	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
    		"https://api.pembayaran.example.com/v1/charge",
    		bytes.NewReader(payload))
    	if err != nil {
    		return err
    	}
    	req.Header.Set("Content-Type", "application/json")
    	req.Header.Set("Authorization", "Bearer "+os.Getenv("PAYMENT_API_KEY"))
    
    	resp, err := httpClient.Do(req)
    	if err != nil {
    		return fmt.Errorf("request gagal: %w", err)
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
    		body, _ := io.ReadAll(resp.Body)
    		return fmt.Errorf("pembayaran ditolak, status %d: %s", resp.StatusCode, body)
    	}
    	return nil
    }

    Urutannya selalu sama: marshal struct jadi byte, bungkus dengan bytes.NewReader, set header Content-Type supaya server tahu isi body-nya JSON, lalu set Authorization dengan kunci API yang diambil dari environment variable. Bukan dari string yang ditulis langsung di kode.

    Tiga jenis kegagalan, tiga perlakuan berbeda

    Saat memanggil API eksternal, error itu bukan satu jenis. Ada tiga, dan perlakuannya beda.

    Error jaringan. httpClient.Do mengembalikan error: DNS gagal, koneksi ditolak, atau timeout. Kamu bahkan tidak dapat respons. Kegagalan seperti ini sering sementara, jadi retry masuk akal.

    Status 4xx. Server menerima request-mu dan menolaknya. 400 berarti body-mu salah format, 401 kunci API salah, 404 endpoint keliru, 422 datanya tidak valid. Ini bug di sisimu. Mengulang request yang sama akan ditolak lagi dengan alasan yang sama. Jangan retry, catat errornya dan perbaiki penyebabnya.

    Status 5xx. Server pihak ketiga sedang bermasalah. Bukan salahmu, dan biasanya sembuh sendiri. Retry pantas di sini, dengan jeda yang membesar seperti pola backoff yang kita pakai untuk email di bagian 21.

    func doDenganRetry(ctx context.Context,
    	buatReq func() (*http.Request, error)) (*http.Response, error) {
    
    	var errTerakhir error
    	for percobaan := 1; percobaan <= 3; percobaan++ {
    		req, err := buatReq()
    		if err != nil {
    			return nil, err
    		}
    
    		resp, err := httpClient.Do(req)
    		if err != nil {
    			errTerakhir = err // error jaringan, coba lagi
    		} else if resp.StatusCode >= 500 {
    			resp.Body.Close()
    			errTerakhir = fmt.Errorf("server membalas %d", resp.StatusCode)
    		} else {
    			return resp, nil // sukses atau 4xx, keduanya final
    		}
    
    		jeda := time.Duration(percobaan*percobaan) * time.Second
    		select {
    		case <-time.After(jeda):
    		case <-ctx.Done():
    			return nil, ctx.Err()
    		}
    	}
    	return nil, fmt.Errorf("menyerah setelah 3 percobaan: %w", errTerakhir)
    }

    Perhatikan dua detail. Request dibuat ulang lewat buatReq di tiap percobaan, karena body request hanya bisa dibaca sekali. Dan jeda backoff dipotong oleh ctx.Done(), jadi kalau user sudah pergi, kita tidak sibuk mengulang untuk siapa-siapa.

    Bungkus jadi client struct yang rapi

    Kalau kode pemanggilan API berserakan di handler, tiap perubahan endpoint memaksa kamu mengedit banyak tempat. Ikuti struktur proyek yang kita susun di bagian 16: bungkus semua urusan satu layanan eksternal ke dalam satu struct.

    type OngkirClient struct {
    	baseURL    string
    	apiKey     string
    	httpClient *http.Client
    }
    
    func NewOngkirClient(baseURL, apiKey string) *OngkirClient {
    	return &OngkirClient{
    		baseURL:    baseURL,
    		apiKey:     apiKey,
    		httpClient: &http.Client{Timeout: 10 * time.Second},
    	}
    }
    
    type Ongkir struct {
    	Kurir    string `json:"kurir"`
    	Biaya    int64  `json:"biaya"`
    	Estimasi string `json:"estimasi"`
    }
    
    func (c *OngkirClient) CekOngkir(ctx context.Context,
    	asal, tujuan string, beratGram int) ([]Ongkir, error) {
    
    	url := fmt.Sprintf("%s/ongkir?asal=%s&tujuan=%s&berat=%d",
    		c.baseURL, asal, tujuan, beratGram)
    
    	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    	if err != nil {
    		return nil, err
    	}
    	req.Header.Set("Authorization", "Bearer "+c.apiKey)
    
    	resp, err := c.httpClient.Do(req)
    	if err != nil {
    		return nil, fmt.Errorf("gagal menghubungi API ongkir: %w", err)
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode != http.StatusOK {
    		return nil, fmt.Errorf("API ongkir membalas status %d", resp.StatusCode)
    	}
    
    	var daftar []Ongkir
    	if err := json.NewDecoder(resp.Body).Decode(&daftar); err != nil {
    		return nil, fmt.Errorf("respons ongkir tidak valid: %w", err)
    	}
    	return daftar, nil
    }

    Perhatikan tipe kembaliannya: []Ongkir, struct domain milikmu sendiri. Bukan []byte, bukan map[string]interface{}. Pemakai client ini tidak perlu tahu bentuk JSON pihak ketiga. Kalau suatu hari kamu ganti penyedia ongkir, cukup ubah isi client ini, seluruh handler tetap aman.

    Bonus besar dari pola ini: gampang dites. Karena baseURL bisa disuntik dari luar, kamu bisa mengarahkannya ke server palsu buatan httptest.NewServer, alat yang sudah kita kenal di bagian 14.

    func TestCekOngkir(t *testing.T) {
    	server := httptest.NewServer(http.HandlerFunc(
    		func(w http.ResponseWriter, r *http.Request) {
    			fmt.Fprint(w, `[{"kurir":"JNE","biaya":9000,"estimasi":"2-3 hari"}]`)
    		}))
    	defer server.Close()
    
    	client := NewOngkirClient(server.URL, "kunci-tes")
    	hasil, err := client.CekOngkir(context.Background(), "purwokerto", "semarang", 1000)
    	if err != nil {
    		t.Fatalf("tidak mengharapkan error: %v", err)
    	}
    	if hasil[0].Kurir != "JNE" {
    		t.Errorf("kurir salah: %s", hasil[0].Kurir)
    	}
    }

    Tes ini jalan tanpa internet, tanpa kunci API asli, dan selesai dalam hitungan milidetik.

    Rahasia di env, dan sopan santun ke pihak ketiga

    Dua aturan terakhir sebelum latihan. Pertama, kunci API adalah rahasia. Simpan di environment variable seperti yang kita biasakan di bagian 18, dan jangan pernah menuliskannya ke log. Baris seperti log.Printf("memanggil API dengan key %s", apiKey) kelihatan sepele saat debugging, tapi log tersimpan lama dan dibaca banyak orang. Log-lah nama layanan dan status, bukan kredensialnya.

    Kedua, hormati rate limit. Hampir semua API komersial membatasi jumlah request per menit, dan membalas 429 kalau kamu kebablasan. Baca dokumentasi limitnya, beri jeda antar request massal, dan kalau kebutuhanmu besar, atur antrean dengan worker pool seperti di bagian 19. Akun yang terus menabrak limit bisa diblokir, dan itu masalah yang jauh lebih mahal daripada menambah satu time.Sleep.

    Latihan: client kurs dipakai dari handler produk

    Sekarang kita gabungkan semuanya. Program di bawah ini punya tiga bagian: server palsu dari httptest yang berperan sebagai API kurs, sebuah KursClient, dan handler /produk yang memakai client itu untuk menghitung harga rupiah.

    package main
    
    import (
    	"context"
    	"encoding/json"
    	"fmt"
    	"log"
    	"net/http"
    	"net/http/httptest"
    	"time"
    )
    
    type KursClient struct {
    	baseURL    string
    	apiKey     string
    	httpClient *http.Client
    }
    
    func NewKursClient(baseURL, apiKey string) *KursClient {
    	return &KursClient{
    		baseURL:    baseURL,
    		apiKey:     apiKey,
    		httpClient: &http.Client{Timeout: 5 * time.Second},
    	}
    }
    
    type Kurs struct {
    	Kode  string  `json:"kode"`
    	Nilai float64 `json:"nilai"`
    }
    
    func (c *KursClient) Ambil(ctx context.Context, kode string) (Kurs, error) {
    	req, err := http.NewRequestWithContext(ctx, http.MethodGet,
    		c.baseURL+"/kurs?kode="+kode, nil)
    	if err != nil {
    		return Kurs{}, err
    	}
    	req.Header.Set("Authorization", "Bearer "+c.apiKey)
    
    	resp, err := c.httpClient.Do(req)
    	if err != nil {
    		return Kurs{}, fmt.Errorf("gagal menghubungi API kurs: %w", err)
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode != http.StatusOK {
    		return Kurs{}, fmt.Errorf("API kurs membalas status %d", resp.StatusCode)
    	}
    
    	var k Kurs
    	if err := json.NewDecoder(resp.Body).Decode(&k); err != nil {
    		return Kurs{}, fmt.Errorf("respons kurs tidak valid: %w", err)
    	}
    	return k, nil
    }
    
    func main() {
    	// API palsu, berperan sebagai penyedia kurs sungguhan
    	palsu := httptest.NewServer(http.HandlerFunc(
    		func(w http.ResponseWriter, r *http.Request) {
    			if r.URL.Query().Get("kode") != "USD" {
    				http.Error(w, `{"error":"kode tidak dikenal"}`, http.StatusNotFound)
    				return
    			}
    			w.Header().Set("Content-Type", "application/json")
    			fmt.Fprint(w, `{"kode":"USD","nilai":16250}`)
    		}))
    	defer palsu.Close()
    
    	client := NewKursClient(palsu.URL, "kunci-rahasia")
    
    	http.HandleFunc("/produk", func(w http.ResponseWriter, r *http.Request) {
    		kurs, err := client.Ambil(r.Context(), "USD")
    		if err != nil {
    			log.Printf("cek kurs gagal: %v", err)
    			http.Error(w, `{"error":"layanan kurs sedang bermasalah"}`,
    				http.StatusBadGateway)
    			return
    		}
    
    		hargaUSD := 25.0
    		w.Header().Set("Content-Type", "application/json")
    		fmt.Fprintf(w, `{"produk":"Lisensi Pro","harga_rupiah":%.0f}`,
    			hargaUSD*kurs.Nilai)
    	})
    
    	log.Println("server jalan di :8080")
    	log.Fatal(http.ListenAndServe(":8080", nil))
    }

    Jalankan dengan go run main.go, lalu coba dari terminal lain. Ini output saat semuanya lancar:

    $ curl localhost:8080/produk
    {"produk":"Lisensi Pro","harga_rupiah":406250}

    Sekarang uji jalur gagalnya. Ubah kode mata uang di handler dari "USD" menjadi "EUR", jalankan ulang, lalu panggil lagi:

    $ curl localhost:8080/produk
    {"error":"layanan kurs sedang bermasalah"}
    
    // di log server:
    2026/07/27 10:15:03 cek kurs gagal: API kurs membalas status 404

    Perhatikan pembagian perannya. User hanya menerima pesan yang sopan dan status 502, sedangkan detail teknisnya masuk ke log untukmu. API pihak ketiga boleh rusak, aplikasimu tetap berdiri dan menjawab dengan jelas. Itu inti dari seluruh bagian ini.

    Sebagai latihan tambahan, ganti httptest.Server di atas dengan API publik sungguhan, atau tambahkan doDenganRetry ke dalam method Ambil supaya error 5xx dicoba ulang otomatis.

    Selanjutnya: bicara serius dengan database

    Aplikasi kita sekarang bisa melayani request, mengirim email, dan memanggil layanan lain. Di bagian 23 kita kembali ke dalam: Transaksi Database dan Migrasi Skema. Kamu akan belajar menjaga data tetap konsisten saat beberapa operasi harus berhasil bersama-sama, dan mengelola perubahan struktur tabel tanpa drama.

    Kalau sistemmu perlu terhubung ke payment gateway, WhatsApp gateway, atau API internal antar divisi, tim Arrazy sudah sering mengerjakan integrasi seperti ini. Lihat layanan pengembangan sistem aplikasi kami untuk berdiskusi soal kebutuhanmu.

  • Docker Port Mapping, Mode Detach, dan Environment Variable

    Docker Port Mapping, Mode Detach, dan Environment Variable

    Container Docker berjalan di jaringan terisolasi, jadi aplikasi di dalamnya tidak otomatis bisa diakses dari browser atau curl di komputermu. Supaya bisa diakses, kamu harus publish port dengan flag -p host:container saat docker run. Itulah inti docker port mapping. Di artikel ini kita praktikkan langsung: menjalankan dua nginx sekaligus di port 8080 dan 8081, menjalankan container di background dengan -d, memberi nama dengan --name, dan menyuntik konfigurasi lewat environment variable -e.

    Artikel ini bagian keempat dari seri Belajar Docker dari Nol. Tiga bagian sebelumnya membahas instalasi, konsep image vs container, dan perintah dasar. Mulai bagian ini kita masuk ke skill yang benar-benar dipakai sehari-hari saat menjalankan aplikasi web di container.

    Prasyarat Sebelum Mulai

    Kamu butuh Docker yang sudah terpasang dan bisa menjalankan docker run hello-world tanpa error. Di artikel ini saya memakai Docker Engine 27 di Ubuntu 24.04, tapi semua perintah berlaku sama di Docker versi 24 ke atas, termasuk di Docker Desktop untuk Windows dan macOS.

    Kamu juga sebaiknya sudah nyaman dengan docker ps, docker stop, dan docker rm. Kalau belum, baca dulu bagian sebelumnya: Perintah Dasar Docker yang Wajib Dikuasai Pemula.

    Kenapa Container Tidak Bisa Diakses Langsung dari Host

    Coba jalankan nginx tanpa flag apa pun:

    docker run nginx:1.27-alpine

    Log nginx muncul di terminal, artinya web server-nya hidup. Tapi kalau kamu buka http://localhost di browser, hasilnya connection refused. Kenapa?

    Secara default Docker menaruh setiap container di jaringan virtual bernama bridge. Container dapat IP internal sendiri, misalnya 172.17.0.2, dan port 80 yang dibuka nginx hanya hidup di IP internal itu. Host alias komputermu tidak otomatis meneruskan traffic ke sana. Ini disengaja. Isolasi jaringan inilah salah satu alasan container aman dijalankan berdampingan tanpa saling ganggu.

    Jembatan antara dunia luar dan container adalah publish port lewat flag -p. Formatnya selalu:

    -p PORT_HOST:PORT_CONTAINER

    Urutannya sering ketukar, jadi hafalkan: host dulu, baru container. -p 8080:80 artinya port 8080 di komputermu diteruskan ke port 80 di dalam container. Port container mengikuti aplikasinya, nginx memang listen di 80. Port host bebas kamu pilih selama belum dipakai proses lain.

    Tekan Ctrl+C dulu untuk menghentikan nginx percobaan tadi sebelum lanjut.

    Praktik Docker Port Mapping: Dua Nginx di Port 8080 dan 8081

    Satu image bisa dijalankan menjadi banyak container sekaligus, asalkan port host-nya berbeda. Kita buktikan dengan dua nginx:

    docker run -d --name web-a -p 8080:80 nginx:1.27-alpine
    docker run -d --name web-b -p 8081:80 nginx:1.27-alpine

    Perhatikan: keduanya sama-sama memakai port 80 di dalam container masing-masing. Tidak bentrok, karena tiap container punya jaringan sendiri. Yang tidak boleh kembar hanya port host, makanya satu dapat 8080 dan satunya 8081.

    Cek keduanya jalan:

    docker ps

    Output yang diharapkan kira-kira seperti ini (ID akan berbeda di komputermu):

    CONTAINER ID   IMAGE               COMMAND                  STATUS         PORTS                                     NAMES
    f3a1c2d4e5b6   nginx:1.27-alpine   "/docker-entrypoint.…"   Up 10 seconds  0.0.0.0:8081->80/tcp, [::]:8081->80/tcp   web-b
    a9b8c7d6e5f4   nginx:1.27-alpine   "/docker-entrypoint.…"   Up 15 seconds  0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   web-a

    Verifikasi dengan curl dan docker port

    Tes dari host dengan curl:

    curl -I http://localhost:8080

    Output yang diharapkan:

    HTTP/1.1 200 OK
    Server: nginx/1.27.5
    Content-Type: text/html
    ...

    Ulangi untuk http://localhost:8081, hasilnya harus sama-sama 200 OK. Dua web server hidup berdampingan di satu mesin, dari satu image yang sama, tanpa konfigurasi virtual host apa pun.

    Kalau lupa container mana dipetakan ke port berapa, tidak perlu menebak dari output docker ps yang padat. Ada perintah khusus:

    docker port web-a

    Output yang diharapkan:

    80/tcp -> 0.0.0.0:8080
    80/tcp -> [::]:8080

    Baris 0.0.0.0 untuk IPv4 dan [::] untuk IPv6. Artinya port 8080 terbuka di semua network interface host. Kalau kamu hanya ingin bisa diakses dari mesin sendiri, misalnya database untuk development, batasi ke localhost: -p 127.0.0.1:8080:80. Kebiasaan kecil ini penting saat nanti kamu deploy ke VPS yang IP-nya publik.

    Mode Detach -d dan Memberi Nama dengan –name

    Di perintah praktik tadi kita sudah menyelipkan dua flag baru. Sekarang kita bedah satu per satu.

    Flag -d (detach) menjalankan container di background. Tanpa -d, terminal kamu tersandera oleh log container, dan menutup terminal atau menekan Ctrl+C ikut mematikan container-nya. Dengan -d, Docker hanya mencetak ID container lalu mengembalikan terminal ke kamu:

    $ docker run -d --name web-c -p 8082:80 nginx:1.27-alpine
    7c9e2f1a8b3d5e6f4a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f

    Container tetap jalan sampai kamu hentikan sendiri dengan docker stop. Hampir semua service seperti web server, database, dan cache dijalankan dengan mode ini.

    Flag --name memberi nama yang kamu tentukan sendiri. Tanpa flag ini Docker mengarang nama acak seperti quirky_banzai, lucu tapi menyulitkan. Bandingkan dua perintah ini:

    docker stop quirky_banzai
    docker stop web-a

    Nama yang jelas membuat stop, logs, port, dan perintah lain gampang diketik dan gampang diingat. Satu aturan penting: nama container harus unik. Selama container bernama web-a masih ada, meskipun statusnya sudah exited, kamu tidak bisa membuat container baru dengan nama sama sebelum menghapusnya dengan docker rm web-a.

    Menyuntik Konfigurasi Lewat Environment Variable -e

    Aplikasi yang baik tidak menulis konfigurasi mati di dalam kode. Password database, mode debug, dan URL API biasanya dibaca dari environment variable. Docker mendukung pola ini lewat flag -e NAMA=nilai saat run.

    Bukti paling sederhana, jalankan container Alpine sekali pakai yang tugasnya cuma mencetak environment:

    docker run --rm -e APP_ENV=production -e APP_PORT=3000 alpine:3.20 env

    Output yang diharapkan:

    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    HOSTNAME=b2c3d4e5f6a7
    APP_ENV=production
    APP_PORT=3000
    HOME=/root

    Dua variabel yang kita suntik ikut muncul. Aplikasi apa pun di dalam container bisa membacanya seperti environment variable biasa.

    Contoh yang lebih nyata adalah image resmi database. MySQL misalnya, wajib diberi tahu password root lewat environment variable, kalau tidak dia menolak start:

    docker run -d --name db-latihan \
      -e MYSQL_ROOT_PASSWORD=rahasia123 \
      -e MYSQL_DATABASE=toko_online \
      -p 3306:3306 \
      mysql:8.4

    Perintah di atas menjalankan MySQL 8.4 dengan password root rahasia123 dan langsung membuatkan database bernama toko_online. Variabel apa saja yang dikenali sebuah image selalu tercantum di halaman image tersebut di Docker Hub, jadi biasakan membaca bagian environment variable sebelum memakai image baru.

    Pola konfigurasi lewat environment variable ini bukan sekadar materi latihan. Di Arrazy, backend Go dan Laravel yang kami bangun untuk sistem aplikasi klien membaca semua kredensial dari environment variable, sehingga image yang sama bisa dipakai di server development maupun production hanya dengan mengganti nilai variabelnya.

    Beda EXPOSE di Image dengan -p Saat Run

    Ini sumber salah paham paling sering di topik port. Saat kamu melihat isi sebuah Dockerfile atau halaman Docker Hub, sering ada baris EXPOSE 80. Banyak pemula mengira baris itu yang membuka port ke host. Bukan.

    EXPOSE hanyalah metadata, semacam catatan dari pembuat image: aplikasi di dalam image ini listen di port 80. Dia tidak membuka apa pun ke host. Tanpa -p, container dengan EXPOSE 80 tetap tidak bisa diakses dari luar.

    Aspek EXPOSE (di Dockerfile) -p (saat docker run)
    Kapan ditulis Saat image dibuat Saat container dijalankan
    Efek ke host Tidak ada, hanya dokumentasi Port host benar-benar diteruskan ke container
    Wajib untuk akses dari luar Tidak Ya
    Bisa diubah pengguna image Tidak, sudah tertanam di image Ya, bebas pilih port host tiap run

    Ada satu titik temu antara keduanya: flag -P (huruf besar). Flag ini menyuruh Docker mem-publish semua port yang tercantum di EXPOSE ke port acak di host:

    docker run -d --name web-acak -P nginx:1.27-alpine
    docker port web-acak

    Output yang diharapkan, angka portnya akan berbeda-beda:

    80/tcp -> 0.0.0.0:32768
    80/tcp -> [::]:32768

    Untuk latihan dan production, -p huruf kecil dengan port eksplisit hampir selalu lebih enak karena portnya bisa ditebak. Anggap -P sebagai trivia yang perlu kamu tahu supaya tidak bingung saat menemuinya di tutorial lain.

    Troubleshooting: Error yang Sering Muncul Saat Publish Port

    Error bind: address already in use

    docker: Error response from daemon: failed to set up container networking:
    driver failed programming external connectivity on endpoint web-a:
    failed to bind host port 0.0.0.0:8080: address already in use

    Artinya port host yang kamu minta sudah dipakai proses lain, bisa container lain, bisa aplikasi biasa seperti Apache atau aplikasi Node yang lupa dimatikan. Cari pelakunya dengan:

    sudo ss -ltnp | grep :8080

    Output contohnya:

    LISTEN 0 4096 *:8080 *:* users:(("docker-proxy",pid=51234,fd=7))

    Kalau yang muncul docker-proxy, pelakunya container lain, cek dengan docker ps lalu stop container yang bentrok. Kalau proses biasa, matikan prosesnya atau lebih aman ganti port host-mu, misalnya jadi -p 8090:80. Di Windows dan macOS, gunakan netstat -ano | findstr :8080 (Windows) atau lsof -i :8080 (macOS) untuk mencari proses yang sama.

    Error the container name is already in use

    docker: Error response from daemon: Conflict. The container name "/web-a" is already
    in use by container "a9b8c7d6e5f4". You have to remove (or rename) that container...

    Nama container harus unik, termasuk container yang sudah berhenti. Lihat semuanya dengan docker ps -a, lalu hapus yang lama dengan docker rm web-a. Kalau masih jalan, hentikan dulu dengan docker stop web-a.

    curl connection refused padahal container jalan

    Tiga penyebab paling umum. Pertama, kamu lupa flag -p sama sekali, cek dengan docker port nama-container, kalau outputnya kosong berarti memang tidak ada port yang dipublish, hapus container lalu run ulang dengan -p. Kedua, kamu mengakses port yang salah, misalnya curl localhost:80 padahal mapping-nya 8080:80, yang diakses dari host selalu port sebelah kiri. Ketiga, urutan -p terbalik, -p 80:8080 berbeda makna dengan -p 8080:80.

    Port sudah dipublish tapi aplikasi belum siap

    Khusus database seperti MySQL, docker ps bisa menunjukkan status Up tapi koneksi masih ditolak beberapa detik pertama. Ini bukan masalah port mapping. Proses inisialisasi database memang butuh waktu. Tunggu sebentar dan cek lognya dengan docker logs db-latihan sampai muncul tulisan ready for connections.

    Bersih-bersih dan Lanjut ke Bagian Berikutnya

    Sebelum lanjut, rapikan semua container latihan supaya tidak makan resource:

    docker stop web-a web-b web-c web-acak db-latihan
    docker rm web-a web-b web-c web-acak db-latihan

    Sampai sini kamu sudah pegang empat senjata baru: -p untuk membuka akses dari host ke container, -d untuk menjalankan di background, --name untuk penamaan yang waras, dan -e untuk menyuntik konfigurasi. Kombinasi keempatnya adalah pola docker run yang akan terus kamu pakai sampai bagian akhir seri ini.

    Bagian berikutnya membahas cara masuk ke dalam container yang sedang berjalan: “Cara Masuk ke Container: docker exec, logs, dan Debugging”. Di sana kamu belajar membuka shell di dalam container, membaca log dengan benar, dan mendiagnosis container yang tiba-tiba mati. Artikelnya terbit menyusul, pantau daftar lengkapnya di halaman hub Belajar Docker dari Nol.

    Referensi

  • Cara Membuat SOP Sederhana untuk Usaha Kecil

    Cara Membuat SOP Sederhana untuk Usaha Kecil

    Coba bayangkan satu skenario sederhana. Kamu liburan 3 hari, HP dimatikan total. Tidak ada telepon, tidak ada chat masuk. Pertanyaannya cuma satu: usahamu tetap jalan atau langsung kacau?

    Kalau jawabannya kacau, jangan buru-buru menyalahkan karyawan. Kemungkinan besar masalahnya bukan di orangnya. Masalahnya, semua cara kerja masih tersimpan di kepalamu. Karyawan bertanya terus bukan karena malas mikir, tapi karena memang tidak ada pegangan tertulis. Setiap keputusan kecil harus lewat kamu dulu.

    Solusinya bukan menambah karyawan atau lembur lebih lama. Solusinya SOP. Dan tenang, SOP untuk usaha kecil itu jauh lebih sederhana dari yang dibayangkan.

    SOP Usaha Kecil Itu Apa Sebenarnya

    SOP usaha kecil adalah instruksi tertulis langkah demi langkah untuk pekerjaan yang berulang. Itu saja. Bukan dokumen formal 50 halaman dengan kop surat, nomor revisi, dan tanda tangan tiga level atasan. Yang seperti itu urusan perusahaan besar.

    Untuk usaha kecil, satu SOP bisa muat di satu halaman. Bahkan kadang cukup setengah halaman. Yang penting, orang baru bisa membaca lalu langsung mengerjakan tanpa harus bertanya ke kamu.

    Ujiannya gampang. Kalau ada karyawan baru masuk besok pagi, apakah dia bisa mengerjakan tugas rutin hanya dengan membaca catatanmu? Kalau bisa, itu sudah SOP. Kalau dia tetap harus nanya kamu lima kali sehari, berarti catatannya belum jadi.

    Banyak pemilik usaha menunda bikin SOP karena merasa usahanya masih kecil. Padahal justru sebaliknya. Semakin kecil timnya, semakin fatal kalau semua ilmu cuma ada di satu kepala. Kamu sakit seminggu, operasional ikut sakit seminggu.

    Ada juga yang menunda karena merasa tidak sempat. Ini alasan yang bisa dimengerti, tapi coba hitung kasar. Berapa menit per hari kamu habiskan untuk menjawab pertanyaan yang itu-itu saja? Kalau totalnya satu jam sehari, artinya kamu kehilangan sekitar 25 jam kerja sebulan hanya untuk mengulang jawaban yang sama. Menulis satu SOP butuh 30 menit. Perbandingannya jelas sekali.

    Mulai dari 5 Pekerjaan yang Paling Sering Ditanyakan

    Kesalahan klasik saat mulai bikin SOP: ingin mendokumentasikan semuanya sekaligus. Hasilnya, semangat habis di minggu pertama dan tidak ada satu pun SOP yang selesai.

    Cara yang lebih masuk akal: mulai dari pekerjaan yang paling sering ditanyakan karyawan ke kamu. Untuk kebanyakan usaha kecil, lima ini biasanya juara:

    • Balas chat pelanggan baru. Format sapaan, info apa yang dikirim duluan, kapan harus follow up. Soal follow up ini bahkan layak dapat perlakuan khusus, karena banyak pelanggan tanya harga lalu menghilang. Kami pernah bahas caranya di artikel cara follow up pelanggan yang tanya harga lalu hilang.
    • Terima dan cek barang datang. Apa yang dicocokkan dengan surat jalan, bagaimana kalau jumlah kurang, ke mana barang disimpan.
    • Tutup kasir harian. Hitung uang fisik, cocokkan dengan catatan penjualan, apa yang dilakukan kalau selisih.
    • Handle komplain. Kalimat pembuka yang menenangkan, komplain mana yang boleh langsung diselesaikan karyawan, mana yang harus naik ke kamu.
    • Posting produk. Format foto, template caption, harga dicantumkan atau tidak, jam posting.

    Lima pekerjaan itu biasanya menyumbang sebagian besar pertanyaan harian yang masuk ke HP kamu. Bereskan lima ini dulu, dan kamu akan langsung merasakan bedanya dalam hitungan minggu. Kalau jenis usahamu berbeda, prinsipnya tetap sama. Catat saja pertanyaan yang masuk selama seminggu, lalu urutkan dari yang paling sering. Itulah daftar SOP pertamamu.

    Format SOP yang Benar-Benar Dipakai Tim

    SOP yang bagus bukan yang paling lengkap, tapi yang benar-benar dibuka saat karyawan bingung. Supaya dibuka, formatnya harus ringan. Cukup lima bagian:

    • Judul. Jelas dan spesifik. Contoh: SOP Menerima Pesanan via WhatsApp.
    • Kapan dipakai. Satu kalimat pemicu. Contoh: gunakan setiap ada chat masuk yang menanyakan produk atau harga.
    • Langkah bernomor. Kalimat perintah pendek. Satu langkah satu aksi. Bukan paragraf panjang.
    • Foto atau screenshot bila perlu. Untuk langkah yang susah dijelaskan dengan kata, misalnya letak menu di aplikasi kasir.
    • Eskalasi. Kalau kejadian di luar langkah di atas, hubungi siapa.

    Biar tidak abstrak, ini contoh SOP utuh yang bisa langsung kamu tiru dan sesuaikan.

    Contoh: SOP Menerima Pesanan via WhatsApp

    Kapan dipakai: setiap ada chat masuk yang menanyakan produk, harga, atau mau memesan.

    • 1. Balas chat maksimal 15 menit di jam kerja. Sapa dengan nama toko: “Halo, dengan Toko Berkah. Ada yang bisa kami bantu?”
    • 2. Tanyakan produk apa yang dicari dan jumlahnya.
    • 3. Kirim foto produk, harga, dan info ongkir sesuai daftar harga terbaru di grup internal.
    • 4. Kalau pelanggan setuju, minta nama, alamat lengkap, dan nomor HP.
    • 5. Kirim rekap pesanan plus total harga, minta pelanggan konfirmasi dengan membalas “OK”.
    • 6. Setelah transfer masuk, cek mutasi rekening dulu, baru tulis pesanan di buku order hari itu.
    • 7. Kirim pesan konfirmasi: pembayaran diterima, pesanan dikirim tanggal berapa.
    • 8. Kalau pelanggan belum membalas rekap dalam 1 hari, kirim satu pesan follow up. Kalau minta diskon di luar ketentuan, teruskan ke owner.

    Perhatikan bentuknya. Setiap langkah adalah kalimat perintah. Pendek. Tidak ada teori. Karyawan baru pun bisa mengikutinya hari pertama masuk.

    Perhatikan juga langkah terakhirnya. Selalu ada jalur eskalasi. Karyawan jadi tahu batasnya: mana yang boleh diputuskan sendiri, mana yang harus naik ke owner. Ini yang membuat kamu tenang melepas pekerjaan tanpa takut ada keputusan liar.

    Kesalahan Umum yang Bikin SOP Cuma Jadi Pajangan

    Banyak usaha kecil sebenarnya sudah pernah bikin SOP. Masalahnya, SOP itu mati muda. Penyebabnya hampir selalu salah satu dari empat ini:

    • Bahasanya terlalu formal. “Karyawan diwajibkan melakukan verifikasi terhadap ketersediaan produk.” Padahal maksudnya cuma “cek dulu stoknya”. Tulis SOP dengan bahasa yang sama seperti kamu ngomong ke tim sehari-hari.
    • Terlalu detail. SOP tiga halaman untuk pekerjaan lima menit tidak akan pernah dibaca. Kalau langkahnya lebih dari sepuluh, kemungkinan besar itu dua pekerjaan yang perlu dipecah jadi dua SOP.
    • Tidak pernah diupdate. Harga berubah, aplikasi ganti, kurir ganti, tapi SOP masih versi tahun lalu. Sekali saja karyawan menemukan isi SOP yang sudah basi, kepercayaan ke seluruh SOP ikut hilang.
    • Disimpan di laptop owner. Ini yang paling sering. SOP rapi tersimpan di folder pribadi yang tidak bisa diakses siapa pun. Simpan di tempat yang gampang dibuka tim: grup WhatsApp yang dipin, Google Doc yang dibagikan, atau dicetak dan ditempel dekat area kerja.

    Perbaiki empat hal ini dan peluang SOP kamu benar-benar hidup naik jauh.

    Membiasakan Tim Pakai SOP Tanpa Drama

    SOP sudah ditulis, disimpan di tempat yang benar. Sekarang bagian tersulit: mengubah kebiasaan. Termasuk kebiasaanmu sendiri. Karena jujur saja, sebagian pemilik usaha diam-diam menikmati posisi jadi pusat segala jawaban. Rasanya dibutuhkan. Tapi harga dari perasaan itu mahal, yaitu usaha yang tidak bisa jalan tanpa kamu.

    Mulai dari satu kalimat sakti: “cek SOP dulu”. Setiap kali karyawan bertanya sesuatu yang sudah ada di SOP, jangan langsung dijawab. Arahkan ke SOP-nya. Awalnya terasa kaku, kadang bikin tidak enak hati. Tapi kalau kamu terus menjawab langsung, tim tidak akan pernah punya alasan membuka dokumen itu.

    Kedua, jadikan setiap kejadian baru sebagai bahan revisi. Ada komplain jenis baru yang belum tercakup di SOP? Selesaikan dulu, lalu tambahkan langkahnya hari itu juga. Dengan begitu SOP tumbuh dari kejadian nyata, bukan dari tebakan.

    Ketiga, libatkan karyawan untuk ikut menulis. Kasir yang setiap hari tutup kasir jauh lebih tahu langkah detailnya daripada kamu. Minta dia menulis draftnya, kamu tinggal merapikan. Bonusnya besar: orang cenderung patuh pada aturan yang ikut dia buat. SOP terasa jadi milik tim, bukan perintah dari atas.

    SOP yang Stabil Adalah Fondasi Sistem Usahamu

    Ada satu manfaat jangka panjang yang jarang disadari. SOP yang sudah stabil dan terbukti jalan adalah bahan terbaik ketika usahamu siap naik kelas ke aplikasi atau sistem digital. Alur yang tadinya manual, misalnya terima pesanan, cek stok, sampai tutup kasir, tinggal diterjemahkan jadi fitur. Pengembang tidak perlu menebak proses bisnismu, karena semuanya sudah tertulis rapi. Kalau kamu mulai berpikir ke arah sana, layanan pembuatan sistem aplikasi kami biasanya justru berangkat dari SOP seperti ini.

    Tapi itu urusan nanti. Untuk minggu ini, targetnya sederhana saja. Pilih satu dari lima pekerjaan di atas, tulis SOP-nya dalam 30 menit, bagikan ke tim, lalu terapkan aturan “cek SOP dulu”.

    Satu SOP per minggu. Dalam sebulan kamu punya empat. Dalam tiga bulan, hampir semua pekerjaan rutin sudah punya pegangan tertulis. Dan saat itu terjadi, liburan 3 hari tanpa HP bukan lagi mimpi yang menakutkan. Usahamu tetap jalan, karena caranya sudah tidak lagi tersimpan hanya di kepalamu.

  • Aplikasi Absensi Karyawan: GPS, Shift, Rekap Otomatis Gajian

    Aplikasi Absensi Karyawan: GPS, Shift, Rekap Otomatis Gajian

    Kalau Anda cari aplikasi absensi karyawan, jawabannya singkat. Karyawan absen dari HP masing-masing, aplikasi mengunci lokasi lewat GPS dan meminta selfie supaya tidak ada yang titip absen. Jam masuk dan jam pulang tercatat otomatis, lengkap dengan status telat atau tepat waktu. Shift bisa diatur dari awal, jadi karyawan pagi dan karyawan malam punya jadwal acuan masing-masing.

    Ujungnya ada di akhir bulan. Semua data telat, lembur, izin, dan cuti sudah terkumpul dalam satu rekap yang siap dipakai untuk hitung gaji. Tidak perlu lagi buka buku absen satu per satu atau scroll grup WA mencari siapa yang izin tanggal berapa. Artikel ini membahas cara kerjanya, kelemahannya yang perlu Anda tahu, dan kapan bisnis Anda sebenarnya belum butuh aplikasi seperti ini.

    Masalah Absensi Manual yang Terasa Pas Gajian

    Absensi kertas atau lapor di grup WA biasanya aman-aman saja di hari biasa. Masalahnya baru terasa saat mau gajian.

    • Tanda tangan dirapel. Karyawan lupa absen tiga hari, lalu tanda tangan sekaligus di akhir minggu. Data jam masuknya jadi tidak bisa dipercaya.
    • Rekap akhir bulan makan waktu berjam-jam. Admin atau pemilik usaha duduk semalaman mencocokkan buku absen, catatan izin, dan chat WA. Salah hitung sedikit, karyawan protes.
    • Titip absen. Satu orang datang, tanda tangan untuk dua orang. Di absensi kertas, ini hampir mustahil dideteksi.
    • Lembur lupa dicatat. Karyawan pulang jam 9 malam, tapi tidak ada bukti tertulis. Akhirnya ribut soal uang lembur yang tidak dibayar.
    • Karyawan lapangan tidak terpantau. Sales atau teknisi bilang sudah di lokasi klien sejak pagi. Benar atau tidak, Anda tidak pernah tahu.

    Semua masalah ini punya akar yang sama. Data kehadiran dicatat manusia, di tempat terpisah, dan baru disatukan belakangan. Aplikasi absensi membalik urutannya. Data tercatat otomatis saat kejadian, langsung masuk satu tempat.

    Cara Kerja Absen GPS dari HP Karyawan

    Konsepnya sederhana. Setiap kantor atau cabang diberi titik koordinat dan radius, misalnya 100 meter. Karyawan buka aplikasi di HP, tekan tombol absen masuk. Aplikasi membaca lokasi HP lewat GPS. Kalau posisinya di dalam radius, absen diterima dan jamnya tercatat. Kalau di luar radius, absen ditolak atau ditandai untuk dicek atasan. Radius ini bisa disesuaikan per lokasi. Kantor kecil cukup 50 meter, pabrik dengan area luas bisa 300 meter atau lebih.

    Untuk tim lapangan, aturannya bisa dibalik. Karyawan boleh absen di mana saja, tapi titik lokasinya ikut tersimpan. Jadi Anda tetap tahu teknisi absen dari alamat klien, bukan dari rumahnya.

    Jujur soal kelemahan: fake GPS

    Perlu diakui, GPS bisa diakali. Ada aplikasi fake GPS yang membuat HP seolah berada di lokasi lain. Karyawan yang niat curang bisa saja memakainya dari rumah.

    Karena itu absen GPS jarang berdiri sendiri. Mitigasinya berlapis. Pertama, wajib selfie saat absen, jadi minimal jelas orangnya yang absen, bukan temannya. Kedua, aplikasi bisa mendeteksi penggunaan mock location di HP Android dan menandai absen tersebut sebagai mencurigakan. Ketiga, atasan tetap bisa melihat pola aneh, misalnya karyawan yang selalu absen tepat di batas radius. Tidak ada sistem yang 100 persen anti curang, tapi kombinasi ini membuat curang jadi jauh lebih repot daripada datang kerja.

    Shift, Tukar Shift, dan Izin yang Tercatat Rapi

    Untuk usaha yang buka pagi sampai malam, absensi tidak ada artinya tanpa pengaturan shift. Aplikasi absensi yang baik membiarkan Anda membuat pola shift sendiri, misalnya shift pagi jam 7 sampai 3, shift sore jam 3 sampai 11. Jadwal tiap karyawan disusun per minggu atau per bulan, dan telat dihitung dari jadwal shiftnya, bukan dari jam kantor umum.

    Tukar shift juga sering jadi sumber kekacauan di sistem manual. Dua karyawan sepakat tukar lewat chat, atasan tidak tahu, rekap akhir bulan jadi tidak cocok. Di aplikasi, pengajuan tukar shift diajukan, disetujui atasan, lalu jadwal berubah otomatis. Ada jejaknya, tidak ada perdebatan. Kalau di akhir bulan ada yang merasa jadwalnya salah, tinggal buka riwayat pengajuan dan selesai.

    Hal yang sama berlaku untuk izin, cuti, dan sakit. Karyawan mengajukan dari aplikasi, lampirkan surat dokter kalau perlu, atasan menyetujui atau menolak dari HP. Sisa jatah cuti terpotong otomatis. Tidak ada lagi kertas izin yang hilang atau cuti yang kelebihan karena tidak ada yang mencatat.

    Rekap Otomatis yang Langsung Nyambung ke Gaji

    Bagian ini yang paling terasa bedanya. Di akhir periode, aplikasi sudah menyiapkan rekap per karyawan. Berapa hari hadir, berapa kali telat dan total menitnya, berapa jam lembur, berapa hari izin, sakit, dan cuti.

    Rekap ini yang jadi dasar hitung gaji. Kalau kebijakan Anda memotong gaji untuk telat di atas 15 menit, datanya sudah ada. Kalau lembur dibayar per jam, total jamnya tinggal dikali tarif. Kebijakan tiap perusahaan berbeda, dan itu wajar. Yang penting datanya akurat dan tersedia tanpa harus direkap manual. Rekapnya juga bisa diekspor ke Excel, jadi kalau selama ini hitung gaji sudah pakai spreadsheet sendiri, tinggal disambungkan tanpa mengubah kebiasaan.

    Di sinilah bedanya aplikasi absensi dengan sekadar mesin fingerprint. Mesin fingerprint hanya mencatat jam. Aplikasi absensi yang terhubung dengan aturan shift, izin, dan kebijakan gaji bisa langsung menghasilkan angka yang siap dibayarkan. Kalau mau selangkah lebih jauh, rekap ini bisa disambungkan ke sistem aplikasi internal lain seperti payroll atau HRIS, jadi slip gaji ikut terbit otomatis.

    Siapa yang Butuh, dan Kapan Kertas Masih Cukup

    Aplikasi absensi paling terasa manfaatnya untuk usaha dengan kondisi seperti ini.

    • Resto atau kafe multi cabang. Owner tidak mungkin ada di semua cabang. Absensi GPS memastikan kru benar-benar hadir di cabangnya masing-masing.
    • Toko dengan sistem shift. Jadwal berganti tiap minggu, tukar shift sering terjadi, hitungan telat harus mengikuti shift.
    • Tim lapangan. Sales, kurir, teknisi. Absen dari lokasi klien dengan bukti koordinat dan selfie.
    • Pabrik atau workshop kecil. Puluhan karyawan, ada lembur rutin, hitungan gaji sensitif terhadap kehadiran.

    Sebaliknya, kalau tim Anda 5 orang dan semuanya duduk di satu ruangan, jujur saja, absensi kertas atau bahkan tanpa absensi formal masih cukup. Anda lihat sendiri siapa yang datang. Beli aplikasi di tahap ini lebih ke soal kerapian administrasi, bukan kebutuhan mendesak. Kembalilah ke sini saat mulai buka cabang kedua atau punya tim lapangan.

    Aplikasi jadi atau bikin custom

    Di pasaran ada banyak aplikasi absensi langganan bulanan. Untuk kebutuhan standar, itu pilihan masuk akal karena cepat dipakai dan biayanya jelas. Aplikasi custom baru layak dipertimbangkan kalau aturan gaji Anda unik dan tidak tertampung di aplikasi jadi, misalnya potongan telat bertingkat, insentif kehadiran khusus, atau perlu nyambung ke sistem lain yang sudah jalan seperti aplikasi kasir atau ERP internal. Dengan custom lewat jasa pembuatan aplikasi mobile, sistemnya mengikuti aturan Anda, bukan sebaliknya. Sekali bangun, tidak ada biaya langganan per karyawan yang membengkak saat tim tumbuh.

    Kalau masih ragu di antara keduanya, ceritakan saja dulu kondisi absensi dan aturan gaji di tempat Anda lewat halaman kontak Arrazy. Dari situ biasanya kelihatan apakah cukup pakai aplikasi jadi atau memang perlu dibangun khusus. Konsultasi awal tidak dipungut biaya.

    Pertanyaan yang Sering Muncul

    HP karyawan saya spesifikasinya rendah, bisa jalan tidak?

    Umumnya bisa. Aplikasi absensi tergolong ringan karena fungsinya hanya membaca lokasi, ambil foto, dan kirim data. Android keluaran 5 tahun terakhir dengan RAM 2 GB biasanya sudah cukup. Yang perlu dicek justru kameranya masih berfungsi untuk selfie dan GPS-nya normal. Kalau ada satu dua karyawan yang HP-nya benar-benar tidak memadai, solusinya bisa lewat satu perangkat bersama di lokasi yang dipakai bergantian.

    Bagaimana kalau sinyal di lokasi jelek?

    Aplikasi yang dirancang baik punya mode offline. Absen tetap tercatat di HP lengkap dengan jam dan lokasinya, lalu terkirim otomatis begitu sinyal kembali. Jadi karyawan di gudang basement atau area pinggiran tetap bisa absen tepat waktu. Yang penting jamnya diambil saat tombol ditekan, bukan saat data terkirim.

    Apakah lokasi karyawan terpantau di luar jam kerja?

    Tidak, dan memang seharusnya tidak. Aplikasi absensi yang wajar hanya membaca lokasi saat karyawan menekan tombol absen. Di luar momen itu, tidak ada pelacakan. Ini penting dijelaskan ke karyawan sejak awal, karena kekhawatiran soal privasi sering jadi alasan penolakan. Absensi GPS itu bukti kehadiran saat jam kerja, bukan alat memata-matai kehidupan pribadi.

    Absensi kelihatannya urusan kecil, sampai dia bikin gajian telat dan karyawan kecewa. Membereskannya lebih awal biasanya jauh lebih murah daripada memperbaiki kepercayaan yang sudah terlanjur rusak.