Bab penutup section ini merangkum praktik terbaik yang telah kita pelajari sepanjang sembilan chapter. Penyebab paling sering dari bug tanggal produksi bukanlah API yang sulit, melainkan pilihan tipe yang keliru: menyimpan LocalDateTime di database, memakai java.util.Date di service layer, atau memilih Duration untuk operasi yang seharusnya memakai Period. Bab ini menyatukan enam pedoman yang harus menjadi default di setiap kode tanggal yang kita tulis.

Enam Pedoman Inti #

#PedomanAlasan
1Simpan momen sebagai Instant di backendUTC eksplisit, tidak ambigu
2Tampilkan sebagai ZonedDateTime di lapisan presentasiMengikuti zona pengguna
3Hindari java.util.Date dan java.util.CalendarAPI lawas, mudah salah
4Gunakan ISO-8601 untuk pertukaran antar sistemStandar, dapat diparse ulang
5Pakai Period untuk satuan kalender, Duration untuk jam absolutMenghormati DST dan panjang bulan
6Untuk Kotlin Multiplatform, gunakan kotlinx-datetimeLintas target JVM/JS/Native

Contoh Lengkap: Enam Pedoman dalam Kode #

Kode berikut menyatukan keenam pedoman ke dalam satu file runnable. Setiap blok dijelaskan setelahnya:

kotlin
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale

// Demonstrasi praktik terbaik pengelolaan tanggal:
// 1. Simpan momen sebagai Instant (UTC) di backend.
// 2. Tampilkan ke pengguna sebagai ZonedDateTime di zona lokal.
// 3. Hindari java.util.Date / java.util.Calendar lawas.
// 4. Gunakan ISO-8601 untuk pertukaran antar sistem.

data class CatatanAudit(
    val id: Long,
    val waktuUtc: Instant,           // selalu UTC
    val pelaku: String,
    val aksi: String,
)

data class TampilanJadwal(
    val judul: String,
    val waktuLokal: String,          // sudah terformat per pengguna
)

fun main() {
    // 1. Penyimpanan: Instant UTC
    val instanTetap: Instant = Instant.parse("2026-01-15T08:30:00Z")
    val catatan = CatatanAudit(
        id = 1,
        waktuUtc = instanTetap,
        pelaku = "Budi",
        aksi = "LOGIN",
    )
    println("=== Praktik 1: Simpan Instant UTC ===")
    println("catatan.waktuUtc : ${catatan.waktuUtc}")
    println("JSON (ISO-8601)  : \"${catatan.waktuUtc}\"")

    // 2. Tampilan: konversi ke zona lokal pengguna
    println()
    println("=== Praktik 2: Tampilkan sebagai ZonedDateTime ===")
    val zonaJakarta: ZoneId = ZoneId.of("Asia/Jakarta")
    val zonaTokyo: ZoneId = ZoneId.of("Asia/Tokyo")
    val zonaNewYork: ZoneId = ZoneId.of("America/New_York")

    val tampilJakarta = formatLokal(catatan.waktuUtc, zonaJakarta, Locale.of("id", "ID"))
    val tampilTokyo = formatLokal(catatan.waktuUtc, zonaTokyo, Locale.JAPAN)
    val tampilNY = formatLokal(catatan.waktuUtc, zonaNewYork, Locale.US)

    println("Jakarta  : $tampilJakarta")
    println("Tokyo    : $tampilTokyo")
    println("New York : $tampilNY")

    // 3. Menghindari API lawas: java.util.Date vs Instant
    println()
    println("=== Praktik 3: Hindari java.util.Date ===")
    val dateLawas = java.util.Date.from(instanTetap)
    val instanDariDate: Instant = dateLawas.toInstant()
    println("java.util.Date   : $dateLawas (zone ambiguitas)")
    println("Date.toInstant() : $instanDariDate (UTC eksplisit)")
    println("setara Instant?  : ${instanDariDate == instanTetap}")

    // 4. Selisih waktu dengan benar (Duration untuk jam, Period untuk kalender)
    println()
    println("=== Praktik 4: Duration vs Period ===")
    val mulai: Instant = Instant.parse("2026-01-15T08:00:00Z")
    val selesai: Instant = Instant.parse("2026-01-15T10:45:00Z")
    val durasiSesi: Duration = Duration.between(mulai, selesai)
    println("durasiSesi       : $durasiSesi (${durasiSesi.toMinutes()} menit)")

    val tanggalAwal: LocalDate = LocalDate.of(2026, 1, 15)
    val tanggalAkhir: LocalDate = LocalDate.of(2026, 3, 2)
    val periodeAktif: java.time.Period = java.time.Period.between(tanggalAwal, tanggalAkhir)
    println("periodeAktif     : $periodeAktif (${periodeAktif.days} hari di komponen Period)")

    // 5. Validasi tanggal input pengguna dengan STRICT resolver
    println()
    println("=== Praktik 5: Validasi input ===")
    val input = "2026-02-30"   // tanggal invalid
    val fmt = DateTimeFormatter.ISO_LOCAL_DATE
        .withResolverStyle(java.time.format.ResolverStyle.STRICT)
    val hasil: LocalDate? = try {
        LocalDate.parse(input, fmt)
    } catch (e: java.time.format.DateTimeParseException) {
        null
    }
    println("input            : $input")
    println("hasil validasi   : ${hasil ?: "ditolak (bukan tanggal valid)"}")

    // 6. Tampilan ZonedDateTime di serialisasi: sertakan zona
    println()
    println("=== Praktik 6: Sertakan zona saat tampilkan ===")
    val zdtJakarta: ZonedDateTime = catatan.waktuUtc.atZone(zonaJakarta)
    val zdtTokyo: ZonedDateTime = catatan.waktuUtc.atZone(zonaTokyo)
    println("zdt Jakarta      : $zdtJakarta")
    println("zdt Tokyo         : $zdtTokyo")
    println("toInstant sama?  : ${zdtJakarta.toInstant() == zdtTokyo.toInstant()}")
}

fun formatLokal(instan: Instant, zona: ZoneId, locale: Locale): String {
    val zdt: ZonedDateTime = instan.atZone(zona)
    val fmt = DateTimeFormatter
        .ofPattern("dd MMM yyyy HH:mm z", locale)
        .withZone(zona)
    return fmt.format(instan)
}

Output yang Dihasilkan:

text
=== Praktik 1: Simpan Instant UTC ===
catatan.waktuUtc : 2026-01-15T08:30:00Z
JSON (ISO-8601)  : "2026-01-15T08:30:00Z"

=== Praktik 2: Tampilkan sebagai ZonedDateTime ===
Jakarta  : 15 Jan 2026 15:30 WIB
Tokyo    : 15 1月 2026 17:30 JST
New York : 15 Jan 2026 03:30 EST

=== Praktik 3: Hindari java.util.Date ===
java.util.Date   : Thu Jan 15 08:30:00 UTC 2026 (zone ambiguitas)
Date.toInstant() : 2026-01-15T08:30:00Z (UTC eksplisit)
setara Instant?  : true

=== Praktik 4: Duration vs Period ===
durasiSesi       : PT2H45M (165 menit)
periodeAktif     : P1M15D (15 hari di komponen Period)

=== Praktik 5: Validasi input ===
input            : 2026-02-30
hasil validasi   : ditolak (bukan tanggal valid)

=== Praktik 6: Sertakan zona saat tampilkan ===
zdt Jakarta      : 2026-01-15T15:30+07:00[Asia/Jakarta]
zdt Tokyo         : 2026-01-15T17:30+09:00[Asia/Tokyo]
toInstant sama?  : true

Mari kita telaah baris-baris kode di atas:

  1. Baris 18-23 (data class CatatanAudit(val waktuUtc: Instant, ...)): model data memakai Instant untuk kolom momen. Saat diserialisasi ke JSON lewat kotlinx.serialization, default formatter menghasilkan ISO-8601 UTC dengan akhiran Z. Pemulihan ke database atau service lain tidak kehilangan informasi zona.
  2. Baris 39-43 (formatLokal(...)): konversi Instant ke ZonedDateTime terjadi di lapisan presentasi. Backend tidak tahu zona pengguna; zona diteruskan sebagai parameter. Tiga zona berbeda menghasilkan tiga tampilan lokal yang berbeda, namun ketiganya merujuk momen yang sama.
  3. Baris 51-54 (java.util.Date.from(...) dan toInstant()): bila terpaksa berinteraksi dengan library lawas yang mengembalikan java.util.Date, segera konversi ke Instant lewat toInstant(). java.util.Date tidak punya zona eksplisit — toString()-nya memakai zona default JVM yang menyembunyikan ambiguitas. Setelah konversi, semua operasi tanggal selanjutnya memakai Instant.
  4. Baris 63-67 (Duration vs Period): mengukur durasi sesi (jam) memakai Duration.between; mengukur panjang langganan (kalender) memakai Period.between. Mencampur keduanya menghasilkan bug tersekat saat DST bergeser.
  5. Baris 75-82 (STRICT resolver): input pengguna harus divalidasi dengan ResolverStyle.STRICT agar 2026-02-30 ditolak, bukan diam-diam digulir ke 2026-02-28. Strategi ini memaksa pengguna memperbaiki input dan mencegah tanggal fiktif masuk database.
  6. Baris 89-92 (sertakan zona saat tampilkan): tampilan ZonedDateTime selalu menyertakan zona (+07:00[Asia/Jakarta]). Kedua ZonedDateTime di atas berasal dari Instant yang sama, terbukti dari toInstant() yang setara. Inilah jaminan bahwa momen tidak terdistorsi saat berpindah zona.

Jebakan Umum dan Cara Menghindari #

Daftar berikut merangkum kesalahan paling sering ditemukan di kode produksi:

  • Menyimpan LocalDateTime di kolom TIMESTAMP database tanpa zona — pulihkan di server zona berbeda menghasilkan jam yang salah. Solusi: kolom TIMESTAMP WITH TIME ZONE + binding Instant.
  • Memakai java.util.Date sebagai tipe parameter API — Date.toString() bergantung zona default JVM. Solusi: ganti dengan Instant.
  • Memakai Duration.ofDays(1) untuk perhitungan kalender — tidak menghormati DST. Solusi: Period.ofDays(1) untuk operasi tanggal.
  • Parsing tanpa STRICT resolver — input 2026-02-30 diam-diam menjadi 2026-02-28. Solusi: withResolverStyle(STRICT).
  • Mengabaikan locale saat formatting — nama bulan tampil dalam bahasa default server. Solusi: withLocale(Locale.of("id","ID")) eksplisit.
  • Meng-hardcode zona di kode backend — tidak portabel. Solusi: zona adalah parameter input, bukan konstanta global.

Strategi untuk Kotlin Multiplatform #

Untuk proyek KMP yang menargetkan JVM + JS + Native, pakai kotlinx-datetime di modul commonMain. Library ini menyediakan subset java.time (Instant, LocalDate, LocalDateTime, Clock) yang bekerja di semua target. Pada modul jvmMain, konversi ke java.time saat diperlukan (mis. untuk DateTimeFormatter kustom atau interaksi dengan library Java).

kotlin Pola: tipe umum di commonMain, java.time di jvmMain
// commonMain: pemodelan data
import kotlinx.datetime.Instant
data class Sesi(val mulai: Instant, val durasi: kotlin.time.Duration)

// jvmMain: formatting untuk HTTP response
import java.time.format.DateTimeFormatter
fun Sesi.formatHttp(): String = ...

Referensi Lanjutan #

  • java.time package — ringkasan paket java.time sebagai basis praktik terbaik tanggal-jam
  • DateTimeFormatter — dokumentasi DateTimeFormatter untuk pola format dan parsing yang konsisten

Penutup Section #

Sembilan chapter di section Date dan Time membangun pemahaman berlapis: dari konsep Instant, LocalDate, ZonedDateTime (chapter 1-5), ke operasi formatting, parsing, dan aritmetika (chapter 6-8), ditutup oleh pedoman praktik terbaik di chapter ini. Section berikutnya — Java Interoperability — akan membahas bagaimana kode Kotlin berinteraksi langsung dengan library Java lawas dan modern, termasuk strategi mengatasi platform types dan anotasi nullability.