P H K

Tue. Jun 16th, 2026 12:12 PM16 mins read
P H K
Source: Gemini Nano Banana - laid off

Per bulan ini gw resmi kembali jadi pengangguran. Gw dan beberapa teman seperjuangan lainnya terdampak layoff dari kantor karena efisiensi. Masalah finansial yang selama ini masih mencekik memaksa perusahaan melakukan layoff. Melihat kondisi kantor selama ini sebenarnya gw ga terlalu kaget. Gw memang sudah memperkirakan kalau keadaan ini ga berubah, cepat atau lambat pasti bakal ada PHK. Gw udah nyiapin dana darurat untuk setahun ke depan dan rutin berinvestasi. Rasa kecewa tentu ada, tapi bagaimanapun juga gw cukup lama di sini dan telah merasakan banyak suka duka.

Awal Bergabung

Waktu itu kantor lama juga mengalami masalah finansial. Perusaahaannya merger dan gaji gw cuma naik Rp500ribu. Gw beberapa kali interview saat itu hingga akhirnya gw diterima di sini. Gw dichat sama teman yang juga abis interview di sini dan ngajak gw apply ke sini. Saat interview gw terkesan sama senior yang menginterview gw. Katanya di sini menerapkan Clean Architecture. Setelah interview dia secara terbuka bilang pemahaman gw bagus. Saat itu gw menjelaskan Design Pattern, SOLID principles, hingga fundamental Java dan Spring dengan baik. Setelah ditimbang-timbang akhirnya gw menerima offering di sini dan berharap skill gw upgrade.

Ngerasa Downgrade

Gw ditempatkan di tim yang tech stacknya jadul, belum ada Clean Architecture, frameworknya terlalu banyak custom, ga sesuai standar umum. Gw merasa downgrade dibanding tempat sebelumnya. Gw sempat bikin story WA “wishing for an upgrade, but get a downgrade instead” trus dibaca sama HR. Pas lolos probation HR ngasih selamat dan di email itu dia bilang kalau ada yang perlu di-upgrade kabari aja apa yang mereka bisa bantu. Kayaknya itu berkaitan dengan story WA gw deh😅.

Bisnis Kompleks, Dokumentasi Minim

Gw cukup awam dengan bisnis ini. Saat onboarding gw cuma dijelasin tentang kultur oleh HR dan tech stack oleh Team Lead. Ga ada onboarding terkait produk. Dokumentasinya outdated. Gw benar-benar buta. Banyak istilah asing gw dengar. Tiap ada yang nanyain tentang bisnis perusahaan tempat gw kerja, gw bingung ngejawabnya. Butuh waktu lama buat gw untuk paham. Ga cuma gw, teman-teman lainnya pun sama. Makanya turnovernya tinggi karena bisnisnya kompleks banget. Ada Team Lead yang resign setelah sebulan doang. Bahkan ada Product Manager yang baru sehari onboarding langsung resign😂. Orang-orang yang tersisa sebelum layoff terakhir menurut gw adalah orang-orang yang benar-benar kompeten di bidangnya dan tahan banting💪. Gw pernah baca di salah satu review ada yang bilang dokumentasi ga tertulis dengan baik. Ini ada benarnya. Seringkali deskripsi task itu cuma bullet points, ga ada story. Misalnya di tiketnya cuma ditulis “bugs di halaman x” tanpa dijelasin detailnya. Dikira gw dukun kali ya bisa menebak bugsnya apa. Gw udah beberapa kali mengeluhkan ini tapi tetap aja gitu. Walau begitu, gw sering solved bugs siluman yang lama ga ke-solved sama yang lain😎.

Pasca Probation

Pasca probation rencananya gw ingin resign aja. Naas, Covid-19 menyerang🦠. Pemerintah memberlakukan WFH selama 2 minggu, eh berlanjut sampai 2 tahun. Perusahaan lain mulai terdampak Covid, layoff mulai terjadi di mana-mana. Keinginan untuk resign gw batalkan. Gw tetap bertahan dengan codebase dinosaurus ini dan perlahan meng-upgradenya dengan pendekatan modern. Gw inisiatif sendiri melakukan refactor di legacy code. Pendekatan yang gw terapkan ini ternyata menginspirasi yang lain😎. Gw mulai betah. Seru juga refactor legacy code dan bermanfaat bagi orang lain.

Mapping Request di Controller

Hal pertama yang gw refactor adalah proses ekstrak request di Controller dari HttpServletRequest. Contohnya seperti ini:

Copy
@RequestMapping("/user/list")
public ResponseEntity<?> list(HttpServletRequest servletRequest) {
	Map<String, String> param = Utilities.constructParam(servletRequest);
	UserListRequest request = new UserListRequest();
	request.setType(param.get("type"));
	//logics
}

Masing-masing parameter diekstrak ke dalam Map lewat utilities class, lalu mapping manual ke dalam objek Data. Di Spring sebenarnya bisa menggunakan Data class pada parameter atau menggunakan anotasi @RequestBody jika menggunakan request body. Codenya gw refactor jadi gini:

Copy
@RequestMapping("/user/list")
public ResponseEntity<?> list(UserListRequest request) {
	//logics
}

@RequestMapping("/user/create")
public ResponseEntity<?> create(@RequestBody UserCreationRequest request) {
	//logics
}

Lebih simple dan ini standar umum. Entah kenapa dulu mereka mapping manual🫣. Sejak gw bikin begini, teman-teman lainnya pada ikut.

Hibernate Validator

Input validation awalnya juga manual sehingga banyak boilerplate. Gw kepikiran menggunakan Hibernate Validator. Kita tinggal pasang anotasi validasi di tiap properti lalu ditrigger otomatis via AOP oleh Spring. Masalahnya beberapa modul ga pake Spring sehingga ga cocok. Gw berinisiatif trigger pake utilities aja agar validasi ini bisa jalan dengan atau tanpa Spring.

Copy
@Value
public class UserDetailRequest {
	@NotBlank
	String username;
	@NotNull
	LocalDate birthDate;
}

Nanti triggernya ga lewat AOP, tapi tiap objek yang mau divalidasi diproses via utilities Validators.

Copy
public void execute(UserDetailRequest requst){
	Validators.validate(request); //trigger Hibernate Validator via utils
	//logics
}

Pemilihan Primary Key

Di perusahaan sebelumnya gw diajarkan untuk menggunakan Surrogate Key sebagai Primary Key menggunakan Sequential ID atau UUID. Natural Key hanya dijadikan Unique Key. Di sini dulu Primary Key sebagian besar pake Natural Key. Kelemahannya adalah ketika itu berupa Composite Key dan berelasi dengan tabel lain maka kita harus mereferensikan key-key tersebut ke tabel lain. Selain itu pendekatan ini ga tahan perubahan. Dulu ada fitur yang ID-nya digenerate otomatis oleh sistem. Lalu ada fitur baru di mana ID tersebut bisa ditulis oleh client lewat file upload. Masalahnya, jika terjadi typo dan ingin mengubahnya maka perlu update banyak tabel secara manual karena ID tersebut berelasi dengan tabel lain. Apalagi di microservices databasenya tiap service bisa beda, Cascade jadi ga berlaku. Gw pun mengajak yang lain agar saat bikin tabel baru pakai Surrogate Key aja sebagai Primary Key.

Legacy Http Client

Saat call ke service lain, kita memanggil utils static method invoke(RequestData, ResponseClass). Masalahnya, itu cuma handle request menggunakan query parameter dengan GET doang🫣. Semua parameter ditampung ke dalam HashMap, jadi kita harus ekstrak lagi dari objek ke Map secara manual🫣. Di sini masih pake Java 8 dan framework jadul. Belum ada HTTP Service Clients Interface seperti di Spring modern. Gw berinisiatif bikin immutable RequestWrapper menggunakan Builder Design Pattern agar bisa menggunakan berbagai opsi request seperti http method, bentuk request, header, content type, dan sebagainya.

Copy
OrderRequest request = new OrderRequest();
Map<String, String> headers = new HashMap()<>;

RequestWrapper listOrder = RequestWrapper.get("/order/list").queryParameter(request).headers(headers).build();
ListOrderResponse listOrderResponse = listOrder.invokes(ListOrderResponse.class);

RequestWrapper updateOrder = RequestWrapper.put("/order/update").requestBody(request).contentType(ContentType.JSON).build();
UpdateOrderResponse updateOrderResponse = updateOrder.invokes(UpdateOrderResponse.class);

API akan dipanggil sesuai opsi yang dipilih. Di sini ga perlu ekstrak request objek manual karena dihandle pakai reflection API. Proses development jadi lebih cepat dan ini sekarang jadi standar😎.

Improve Utilities

Di sini ada modul yang berisi utilities kayak Apache Commons gitu. Contohnya saat konversi String ke int dengan default value biasanya codenya gini:

Copy
String str = "";
int num = StringUtils.isValidInt(str) ? Integer.parseInt(str) : 0;

Padahal bisa dibikin simple jadi gini:

Copy
int num = StringUtils.toInt(str); //default value 0
int numWithDefault = StringUtils.toInt(str, 100); //default value 100

public static int toInt(String input, int defaultVal){
	try{
		return Integer.parseInt(input);
	} catch (Exception e){
		return defaultVal;
	}
}

public static int toInt(String input){
	return toInt(input, 0);
}

Gw juga banyak melakukan improvement terhadap utilities semacam ini di utils lainnya.

Constant Collection

Ketika ada case menggunakan constant Collection biasanya menggunakan ArrayList atau HashMap. Masalahnya itu mutable, ga cocok jadi constant. Selain itu, ga semuanya butuh List, malah seringnya Set karena constant biasanya unik. Di Java 8 belum ada method semacam List.of(), Set.of(), atau Map.of(). Gw inisiatif bikin Utils immutable collection kayak gini:

Copy
public static final List<String> STRING_LIST = CollectionUtils.listOf("satu","lima");

public static final Set<Integer> UNIQUE_NUMBERS = CollectionUtils.setOf(1, 3, 2);

public static final Map<Integer, String> IMMUTABLE_MAP = CollectionUtils.mapOf(map -> map
		.put(1, "satu")
		.put(3, "tiga")
		.put(2, "dua")
);

Centralized Exception

Di Controller juga banyak sarang Exception handling di tiap endpoint. Padahal handlingnya seragam.

Copy
@RequestMapping("/user/create")
public ResponseEntity<?> create(UserCreationRequest request) {
	try{
		//logics
	} catch(InvalidRequestException e){
		//handle response invalid request
	} catch(ConstraintViolationException e){
		//handle response constraint violation
	} catch(Exception e){
		//handle response server error
	}
}

//same pattern with other endpoints

Padahal Spring ada fitur handling terpusat pakai anotasi @ControllerAdvice.

Copy
@ControllerAdvice
public class ExceptionHandlerController{

	@ExceptionHandler(InvalidRequestException.class)
	public ResponseEntity<?> handleInvalidRequestException(InvalidRequestException ex, HttpServletRequest request){
		ResponseStatus status = ResponseStatus.INVALID_REQUEST;
		printLog(ex, request, status);
		return buildResponseEntity(status, ex.getMessage());
	}

//same pattern with ConstraintViolationException and others
}

Setelah bikin Controller Advice, Controller jadi lebih rapi.

Copy
@RequestMapping("/user/create")
public ResponseEntity<?> create(UserCreationRequest request) {
	//logics
}

//same pattern with other endpoints

Routing API

Ada satu modul legacy di mana routingnya ga lewat Spring Controller, melainkan lewat static method berdasarkan path.

Copy
public static Handler getHandler(String path, Map<String, String> payload){
	switch (path) {
		case "/order/list":
			return new OrderListHandler(payload);
	}
}

Misalkan ada API yang call ke endpoint /order/list, maka akan diarahkan ke OrderListHandler. Semua endpoint di modul itu kayak gini routingnya menuju service handler. Masalahnya, ini jadi god class. Puncaknya, modul ini jadi ga bisa compile karena kegendutan. Method-nya tembus 6000+ line😱. Salah satu senior memindahkan beberapa handler dari switch case ke Map Factory. Untuk sementara waktu ini aman dan bisa di-compile lagi. Tapi tetap aja, ini masih god class. Kalau gw refactor pakai Spring Controller maka jadi ga backward-compatible. Gw inisiatif bikin anotasi untuk routing langsung dari service class.

Copy
@HandlerPath(path = "/order/list", httpMethod = HttpMethod.GET)
@EnumPayload(name = "status", type = OrderStatusEnum.class)
@BasePayload(name = "keyword", type = ParameterValueType.STRING)
@DatePayload(name = "fromDate", type = "dd/MM/yyyy")
@JsonPayload(name = "items", type = OrderItem.class)
public class OrderListHandler {
//service class logic
}

Dengan anotasi @HandlerPath gw menggunakan Event Listener agar saat startup Spring mendaftarkan path /order/list dan objek OrderListHandler ini ke HashMap yang dibikin senior sebelumnya. Udah ga perlu lagi nulis switch case panjang, semuanya kini tinggal ditulis langsung di service class. Sekarang inovasi gw ini jadi standar buat endpoint baru di service ini. Lalu gw dapat ide buat improve anotasi itu. Dulu, semua handler by default bisa menerima http method apa pun. Jadi saat create menggunakan GET dulu bisa-bisa aja😅. Gw improve dengan menambahkan opsi httpMethod di anotasi dan divalidasi. Gw tambahin juga anotasi untuk validasi payload seperti @EnumPayload, @BasePayload, @DatePayload, dan @JsonPayload karena di service itu mapping request masih manual sehingga ga memungkinkan pake Hibernate Validator. Masalah lainnya adalah terkait dokumentasi API. Gw pun bikin API docs sendiri mirip Swagger. API docs bikinan gw ini ngebaca anotasi di class itu yang bisa diakses lewat endpoint dokumentasi.

Mutable Singleton Bugs

Dulu objek handler ini bukan Singleton, jadi global variabel bebas dimutasi. Saat handler ini dimigrasi ke map factory & lewat anotasi otomatis ini jadi Singleton. Masalahnya banyak junior yang belum ngerti design pattern ini. Hasilnya ada bugs race condition🤯. Untungnya gw ngeh dan gw refactor semua handler jadi immutable. Gw share juga ini ke dev lain biar ga keulang lagi.

PDF Generator

Salah satu legacy yang gw tinggalkan adalah cara bikin PDF. Dulu, bikin PDF itu lewat API Itext-PDF. Tiap bikin satu fitur PDF baru butuh 13 SP karena API-nya terlalu kompleks dan ga intuitif untuk desain. Apesnya saat gw dapat task bikin invoice dengan desain yang rumit seperti kolom dengan garis putus-putus, kolom yang digabung, logo client, font yang beda, kolom tanda tangan yang bisa di-custom, dan sebagainya. Gw nyerah make API itu. Setelah keliling stackoverflow gw dapat ide buat bikin PDF dari HTML. Gw bikin modul baru untuk integrasi antara objek Data, Thymeleaf untuk bikin HTML, dan library untuk convert dari HTML ke PDF. Jadi dari objek Data kita perlu generate HTML dulu sebelum convert ke PDF. Sebelumnya dari Java langsung ke PDF. Sekarang jadi ada 3 layer dari Java -> HTML -> PDF. Namun, secara penggunaan ini lebih praktis karena dengan HTML kita bisa bikin desain dengan gampang. Story point untuk bikin PDF yang biasanya 13 SP jadi 2-5 SP doang. Modul HTML-PDF buatan gw ini sekarang jadi standar untuk fitur PDF😎. Selain PDF, modul ini juga gw kembangkan buat bikin template email HTML.

@LegacyTransactional Annotation

Dulu transaksi di legacy ga atomic. Misalkan ada flow untuk insert order dan order items, tapi setelah insert order terjadi error, maka data order yang sudah di-insert ini ga bisa rollback. Kalau di Spring Data JPA kita disuguhkan anotasi @Transactional. Gw berinovasi bikin anotasi sendiri mirip @Transactional. Kebetulan sebelumnya gw sempat mempelajari cara kerja anotasi ini. Gw bikin modul baru, legacy-transaction yang berisi anotasi buatan gw, @LegacyTransactional beserta implementasinya menggunakan AOP. Cara kerjanya gw bikin session di method yang ada anotasi ini lewat AOP dan session itu gw simpan di ThreadLocal. Gw bikin Repository V2 yang bertugas mengeksekusi query menggunakan session dari ThreadLocal yang tersimpan. Tiap sebelum keluar dari method itu dia akan commit jika sukses atau rollback jika error lewat AOP. Sekarang method yang ada anotasi ini jadi atomic transaksinya. Gw tambahin juga properti isReadOnly untuk routing koneksi ke read replica. Awalnya sesederhana itu aja. Kemudian ada masalah lain, yaitu ketika transaksi gagal, misalnya karena locking, dia langsung rollback dan error. Ga ada opsi retry sehingga user harus hit manual lagi. Gw inisiatif nambahin opsi retry di anotasi itu. Jika transaksi gagal dengan Exception tertentu maka dia akan retry dulu sebanyak value yang diisi dengan delay eksponensial. Selang beberapa bulan kemudian gw menemukan bugs terkait Write Skew. Ini sering terjadi secara random selama beberapa tahun. Intinya ada perubahan yang bergantung pada data lain yang berubah sehingga terjadi anomaly. Gw tambahin juga opsi isolation beserta lama lock timeoutnya untuk mengatasi ini. Jadi lewat satu anotasi ini gw menyelesaikan beberapa permasalahan😎. Bentuknya kayak gini:

Copy
@LegacyTransactional(isolation = Isolation.SERIALIZABLE, lockTimeout = 1,
		retry = @RetryTransaction(on = {LockAcquisitionException.class, StaleObjectStateException.class}, max = 3, delay = 2))
public Response update() {
//update logic
}

@LegacyTransactional(isReadOnly = true, isolation = Isolation.REPEATABLE_READ)
public Response list() {
//list logic
}

Di service ini sekarang inovasi gw ini juga jadi standar untuk endpoint baru😎.

Numeric Pagination

Waktu itu di production tiba-tiba halaman yang ada paging mendadak berat. Kebetulan sebelumnya gw pernah bahas tentang pagination di blog jadi gw langsung paham masalah & solusinya😎. Gw tawarin 3 opsi solusi ke Product Manager. Waktu itu Product Manager prefer filter menggunakan tanggal karena developmentnya lebih kecil dan dia belum mau mengubah UI/UX. Kita sepakati date range filternya max 31 hari aja.

Batch Upload Processor

Ini adalah modul untuk meng-upload data secara batch menggunakan csv atau xlsx. Di modul ini juga menggunakan switch case yang panjang tiap fitur🫣. Bedanya, belum nyampe ribuan baris aja sehingga belum compile error. Cepat atau lambat suatu saat bakal bermasalah. Modul ini ga pake Spring, cara paling bersih yang kepikiran ya dengan Map Factory. Masalah lainnya di modul ini adalah ga ada unit test. Class di modul ini banyak pake static method, reuse code lewat inheritance, dan banyak hidden dependency sehingga susah dibikin unit testnya🫣. Gw inisiatif membuat pendekatan baru. Class processor yang baru gw bikin dengan dependency inversion. Reuse code gw ubah lewat composition. Gw bikin adapter untuk reuse code dari legacy processor lewat dependency injection. Dengan begini objeknya bisa di-mock dengan mudah untuk unit test. Gw juga bikin wrapper untuk processor ini. Dengan wrapper, validasi input yang tadinya manual gw ganti pakai anotasi Hibernate Validator. Wrapper tersebut juga bisa handle concurrency, proses validasi banyak data sebelumnya synchronous gw ganti jadi asynchronous. Proses batch upload yang tadinya cuma bisa baca file dari S3 AWS gw tambahin opsi buat baca file dari lokal untuk memudahkan debug saat development.

Auto-Register Hibernate Entity

Di legacy service, entity hibernate itu harus di-register manual. Tiap ada tabel baru maka classnya harus didaftarin dulu satu-satu di xml. Ini kadang sering ketinggalan saat development🫣. Kalau ada yang commit register entity di xml yang sama sering juga terjadi git conflict. Di Spring Data JPA, semua class yang ada anotasi @Entity akan didaftarkan otomatis. Gw inisiatif bikin kayak gitu. Gw pake library Reflections untuk scan semua class yang ada anotasi @Entity lalu didaftarin otomatis ke Hibernate😎. Ga ada lagi pendaftaran entity manual. Ga ada lagi git conflict saat ada orang lain commit entity baru secara bersamaan.

Analytics Via ETL Pipeline (OLTP-OLAP)

Di sini ada 2 DB, OLTP untuk data transaksi, dan OLAP untuk data analitik. Sempat ada wacana pake Clickhouse atau AWS Redshift untuk analitik, tapi pada akhirnya kita menggunakan MySql. Dulu data analitik ini cuma data Denormalized dari OLTP untuk mengurangi join. Seiring waktu datanya makin kompleks. Lama-lama lambat juga, bisa semenit saat query. Gw ditugaskan mengganti pendekatan untuk menampilkan data analitik menggunakan data agregasi harian & bulanan. Misalnya data revenue harian. Jadi, tiap ada transaksi nanti akan dihitung data agregasi revenue di hari itu di DB analitik via ETL Pipeline. Ketika membaca data tahunan, maka yang dikalkulasi nanti hanya 365 baris aja, bukan jutaan baris lagi per client karena data harian sudah diakumulasikan per baris di DB. Query yang tadinya lebih dari semenit sekarang hanya hitungan millisecond⚡.

KPI Calculation

Masih terkait analitik, gw juga pernah ngerjain projek terkait KPI Calculation. Di sini query-nya dinamis sesuai konfigurasi. Logic query-nya ribet. Codenya semrawut😵‍💫. Gw inisiatif pakai QueryDsl biar query-nya fleksibel. Logic query dihandle pakai Java dan outputnya berupa query yang dieksekusi ke DB. Codenya jadi lebih readable dan bisa dibongkar-pasang. Teman-teman lainnya pun ikutan bikin query pake QueryDsl.

Clean Architecture

Pada akhirnya semua service terutama endpoint baru wajib dibuat menggunakan Clean Architecture. Use Case dibuat menggunakan Command Design Pattern dan Gateway implementation dibuat menggunakan Façade Design Pattern. Gw belajar banyak tentang ini. Masalahnya, di modul legacy struktur modulnya ga sesuai Clean Architecture di mana Persistence & Framework harusnya berada di layer terluar. Di sini layer Persistence berada di dalam layer Use Case sehingga banyak yang menyalahgunakannya. Ada satu service yang arsitekturnya benar-benar beda sendiri dibikin sama Team Leadnya. Ada code framework di dalam Use Case. Ada business logic di Gateway dan Presenter. Interface yang harusnya Single Responsibility malah dibikin hierarki untuk semua hal kayak BaseUseCase, BasePresenter, BaseRepository, dan sejenisnya. Hal ini justru merepotkan karena over-abstraction🫣.

Unit Test

Selain Clean Architecture, semuanya juga mulai diwajibkan bikin unit test. Gw teryakinkan bahwa unit test itu menjamin kualitas code. Yang jadi kendala adalah di legacy code banyak call ke static method dan banyak hidden dependency sehingga ribet mockingnya. Untuk endpoint baru karena udah pakai Clean Code sih aman. Setelah beberapa eksplorasi gw merasa pendekatan menggunakan BDD yang paling gw suka dengan Soft Assertion dari AssertJ. Sebisa mungkin gw bikin unit test coveragenya di atas 90%. Apalagi sejak ada AI gw bisa minta buatin test case yang lengkap😎.

Ngoding WebFlux

Di sini ada salah satu service yang menggunakan Framework Reactive, yaitu WebFlux. Singkatnya, ini menggunakan Event Loop seperti di JavaScript untuk eksekusi logic secara paralel sehingga lebih kecil resource yang dibutuhkan daripada menggunakan Thread biasa yang boros memori. Paradigmanya menggunakan Functional Programming. Syntaxnya cukup verbose dan sulit dibaca. Gw pribadi kurang sreg ngeliat code reactive ini.

Traveling Salesman Problem

Salah satu tantangan terbesar gw di sini adalah saat develop fitur Traveling Salesman Problem, yaitu algoritma untuk mencari rute terbaik yang dilalui driver untuk mengambil dan mengantarkan barang. Gw pake library Jsprit. Sebenarnya itu cukup lengkap fiturnya seperti multiple warehouse, window time pick up & delivery, material type, quantity limit, dan banyak lainnya. Masalahnya, client ingin custom fitur yang ga ada di Jsprit. Misalnya mereka ingin driver break di jam tertentu beberapa kali, atau pengantaran di customer yang sama harus dianterin oleh driver yang sama. Kita udah coba memenuhi permintaan custom mereka dengan menambahkan algoritma sendiri. Seringkali client ngerasa rute yang digenerate ga sesuai dan versi mereka lebih baik. Padahal setelah dibandingkan jauh lebih efisien rute yang kita generate. Kadang rute versi client itu human error dan ga sesuai constraint seperti window time, over capacity, atau ga sesuai jenis kendaraan yang dipilih. Gw cukup stres ngerjain ini yang ga ada ujungnya🥴.

Sales Tracking Problem

Di sini sempat ada bugs terkait sales tracking saat check in & check out di toko. Jarak tempuh sales jadi ngaco🛵. Bugs ini berlangsung intermitten selama bertahun-tahun tanpa tau penyebabnya🪲. Ada 3 root cause yang gw temukan. Pertama, ada user yang check in & check out pake 2 hp sehingga user bisa check in di 2 toko berbeda bersamaan tanpa check out dulu. Kemudian ada juga user yang menggunakan Fake GPS sehingga jarak tempuhnya tercatat dari Moscow ke Jakarta. Lalu ada scheduler auto check out jam 23:59 yang id-nya race condition sehingga kadang ga tereksekusi. Gw berhasil fixing & mencegah bugs tersebut😎.

Begadang Tanpa Hasil

Salah satu momen yang cukup lucu buat gw adalah saat ada fitur urgent untuk kebutuhan training. Ini adalah fitur lama yang sempat dihapus dan ingin diaktifkan lagi dengan lebih matang. Gw kelar bikin fitur itu H-2 sebelum rilis. H-1 sore harinya mobile engineer baru mulai ngerjain. Karena jam kerjanya fleksibel, dia emang biasa kerja mulai sore sampai malam. Gw terpaksa ikut begadang stand by nungguin dia😓. Padahal dari pagi gw gabut. Ternyata ada beberapa hal yang perlu ditambahkan. Gw pun develop fitur tambahan itu malam itu juga. Sekitar jam 1 malam baru kelar. QA pun mulai testing hingga semuanya passed sekitar jam 5 pagi. Gw langsung deploy dan lanjut tidur🛌. Jam 9 pagi gw bangun dan kembali stand by🥱. Ternyata ada masalah di fitur itu, aplikasinya nge-blank. Gw cek log ga ada tanda-tanda request masuk. Udah pasti masalahnya di mobile. Pas dicek ternyata ada hardcode di mobile agar hanya client tertentu yang bisa make fitur itu. Saat testing kebetulan QA menggunakan akun client yang di-hardcode itu sehingga ga ada masalah. Pada akhirnya training itu ditunda di lain hari. Percuma gw bela-belain begadang🤣.

Migrasi Message Queue Menggunakan Redis Stream

Sebelumnya di sini proses queue menggunakan AWS SQS yang dibayar per request. Demi efisiensi, ini perlu diganti. Pilihannya adalah menggunakan Redis instance yang udah ada sehingga ga perlu biaya infrastruktur baru. Awalnya gw diberi tugas migrasi ke Redis PubSub. Kebetulan sebelumnya gw pernah riset tentang event driven menggunakan Redis PubSub dan Redis Stream. Gw menawarkan migrasi pake Redis Stream aja karena PubSub itu sistemnya fire-and-forget, ga cocok buat queue. Ini adalah inisiatif terakhir gw sebelum kena layoff🥹.

Pengalaman Positif

Selama di sini gw mendapat pengalaman ngehandle aplikasi SaaS multi-tenant dengan ribuan klien. Tiap klien punya user terdaftar kisaran puluhan hingga ribuan. Transaksinya kisaran ribuan hingga puluhan ribu TPM dengan p95 latency di beberapa service kisaran 50ms hingga 300ms berdasarkan data Kibana terakhir yang gw baca. Ga sebesar aplikasi B2C sih, tapi cukup menambah pengalaman gw. Rekan kerja saling backup dan saling bantu saat ada masalah. Ga ada blaming culture. Hampir semuanya bertanggung jawab dengan kerjaan masing-masing dan memiliki jiwa ownership yang tinggi. Mereka cukup terbuka dengan pendapat dan masukan yang diberikan. Mereka cukup mengapresiasi kontribusi gw. Kultur kayak gini yang bikin gw nyaman di sini.

Masa Sulit

Saat awal Covid sempat ada kebijakan Unpaid Leave. Gaji dikurangi, tapi ada jatah cuti tambahan tiap bulan. Awalnya gw masih ok sama kebijakan ini karena cukup fair. Setahun berikutnya setelah lebaran terjadi gelombang layoff pertama. Di periode ini gw masih aman. Beberapa client ga melanjutkan kerjasama. Beberapa calon client besar gagal bergabung. Sempat ada kabar suntikan investor baru, tapi batal masuk. Gw mulai was-was bahwa kondisi ini bakal makin memburuk. Gaji dipotong lagi. Layoff gelombang kedua kembali terjadi. Gw masih aman. Laptop gw didowngrade ke Core i5 keluaran 2014 dengan RAM 8GB doang. Gaji & THR mulai telat, dirapel, hingga terhutang. BPJS nunggak. Cuti bersama jadi motong cuti tahunan. Di tahap ini gw udah demotivasi. Gw yang sebelumnya masih senang berinovasi dan inisiatif melakukan improvement mulai malas-malasan. Udah ga ada semangat lagi buat brainstorm. Chat mulai slow response. Lalu ada beberapa layoff kecil-kecilan yang berdampak pada segelintir orang. Hiring udah ga ada. Semua kerjaan dibebankan ke 2 tim engineering kecil yang tersisa. Kerjaan numpuk karena kurang SDM sementara gaji dipotong. Gw merasa kondisi ini udah benar-benar gila.

Akhir Perjalanan

Selama 2 tahun terakhir gw ga melihat tanda-tanda perubahan yang lebih baik. Gw memang udah memperkirakan PHK hanya soal waktu. Hingga tanggal 13 April lalu gw diajak one-on-one dadakan sama CTO. Gw dikabari bahwa gw salah satu dari yang terdampak layoff kali ini. Gw dikasih waktu buat hand-over hingga akhir Mei. Gw sebelumnya memang berniat resign setelah lebaran dan memesan tiket ke Jakarta jauh-jauh hari. Secara mental gw udah siap ini bakal terjadi. Malangnya, hilal jadwal pembayaran pesangon masih belum terlihat😭. Klaim JKP & JHT jadi ga bisa karena kantor masih nunggak😭. Sekarang gw masih berusaha mencari pekerjaan di tengah kondisi job market yang lagi anyep. Inginnya sih gw kerja di tempat yang lebih stabil. Semoga gw segera menemukannya🤲.

© 2026 · Ferry Suhandri