Menghindari Duplikasi dan Race Condition Saat Batch Import di Aplikasi WMS

@fakhrulnugrohoJuly 18, 2025

Saat kita bekerja dengan sistem Warehouse Management System (WMS), salah satu tantangan yang sering muncul adalah bagaimana menjaga agar proses import data secara bersamaan oleh banyak user tidak menyebabkan duplikasi data di database. Apalagi kalau kita dealing dengan sesuatu yang kritikal seperti box code, yang harus benar-benar unik dan tidak boleh ganda.


Studi Kasus: Import XLSX Inbound

Misalnya, kita punya sebuah fitur import file .xlsx yang berisi banyak box, produk, dan serial number. Bisa dibilang ini adalah proses batch import yang dilakukan oleh user.

Contoh data yang di-import:

CSV
boxCode,productCode,serialNumber
BOX001,P001,SN001
BOX001,P002,SN002
BOX002,P003,SN003

Masalah muncul ketika:

  1. Dua user atau dua thread mencoba meng-import data dengan boxCode yang sama.
  2. Proses ini bisa berjalan bersamaan.
  3. Kalau tidak ada proteksi, maka dua-duanya bisa sukses menyimpan BOX001, dan kita punya duplikasi.

Solusi: Redis Lock per Box

Untuk menyelesaikan masalah ini, kita menggunakan Redis distributed lock (via Redisson). Idenya adalah:

  1. Ambil semua boxCode dari file import.
  2. Kunci masing-masing boxCode di Redis.
  3. Kalau gagal mengunci (karena sedang dikunci oleh proses lain), langsung throw error.
  4. Setelah proses selesai, unlock semua lock.

Implementasi di Spring Boot

JAVA
@Transactional
public List<Inbound> importXlsx(InboundBulkDTO body, User user) throws IOException {
    List<InboundImportXLSX> inboundImportXLSXList = this.extractXlsx(body);
    Set<String> boxCodes = new HashSet<>(inboundImportXLSXList.stream()
        .map(InboundImportXLSX::getBoxCode)
        .distinct()
        .toList());
    Map<String, RLock> boxLocks = new HashMap<>();

    try {
        for(String boxCode : boxCodes) {
            RLock boxLock = redissonClient.getLock(
                String.format("lock:box:%s:%s", body.getWarehouseId(), boxCode));
            boolean isLocked = boxLock.tryLock(0, 30, TimeUnit.SECONDS);
            if(!isLocked)
                throw new BadRequestException(String.format("Box %s already on process!", boxCode));
            boxLocks.put(boxCode, boxLock);
        }
        List<Inbound> inbounds = this.processImport(inboundImportXLSXList, user, body.getWarehouseId());
        return inbounds;
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        boxLocks.values().forEach(boxLock -> {
            if (boxLock.isHeldByCurrentThread()) boxLock.unlock();
        });
    }
}

Keuntungan Pendekatan Ini

Thread-safe: Proses import oleh banyak user tetap aman.

Atomic: Kalau gagal lock salah satu box, proses tidak dilanjutkan.

Transactional: Dengan bantuan anotasi @Transactional, proses rollback terjadi otomatis jika ada error.

Reusable: Kunci berbasis Redis bisa dipakai untuk proses lain seperti inbound/outbound/pickpack, dll.


Catatan Tambahan


Penutup

Dengan pendekatan ini, proses import batch kita jadi jauh lebih solid. Redis di sini jadi seperti satpam virtual yang jaga supaya gak ada box yang diinput dua kali oleh user berbeda.

Kalau kamu kerja di sistem distribusi besar seperti WMS, trik ini wajib banget dikuasai!

"Better safe than sorry. Lock it before you drop it." 🚀