P H K

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 dari tahun sebelumnya kalau keadaan ini ga berubah, cepat atau lambat pasti bakal layoff. Rasa kecewa tentu ada, tapi bagaimanapun juga gw cukup lama di sini dan telah merasakan banyak suka duka selama di sini.
Awal Bergabung
Waktu itu kantor lama juga mengalami masalah finansial. Bedanya, dulu gw ga kena layoff, hanya dipindahkan ke subsidiary lain dan gaji gw cuma naik Rp500ribu. Gw beberapa kali ke “dokter wawan” saat itu hingga akhirnya gw diterima di sini. Gw dichat sama teman gw yang 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 gw bagus banget. Saat itu gw menjelaskan Design Pattern, SOLID principles, hingga fundamental Java, OOP, dan Spring Boot dengan baik. Gw sempat di-counter offer sama perusahaan Bank Syariah yang saat itu juga lagi proses offering, tapi perusahaan ini juga counter offer. Setelah ditimbang-timbang akhirnya gw memilih di sini. Gw penasaran dengan arsitekturnya dan berharap skill gw upgrade.
Ngerasa Downgrade
Gw ditempatkan di tim yang tech stacknya jadul, belum ada Clean Architecture, modulnya terlalu banyak custom, ga sesuai standar umum. Frameworknya Spring Boot, tapi banyak hal masih manual. Banyak hidden dependency dan menggunakan static method. Best practice ga banyak diterapkan. try/catch bertebaran di Controller. Layer modulnya salah kamar. Transaksi database ga atomic. Dan masih banyak lagi. Itu justru malah downgrade buat gw dibanding tempat sebelumnya. Gw sempat bikin status WA “wishing for an upgrade, but get a downgrade instead” trus dibaca sama HR. Pas gw lolos probation HR ngasih selamat dan di email itu dia mention kalau ada yang perlu di-upgrade kabari aja apa yang mereka bisa bantu. Kayaknya itu berkaitan dengan status 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. Dokumentasi bisnisnya saat itu outdated dan ga lengkap. Gw benar-benar buta. Banyak istilah-istilah yang baru gw dengar. Ketika ada yang nanyain tentang perusahaan tempat gw kerja, gw bingung ngejawabnya. Butuh waktu yang cukup lama buat gw untuk paham. Ga cuma gw, teman-teman yang lain pun pada bingung. Makanya di sini turnovernya dulu cukup tinggi karena bisnisnya kompleks banget. Ada Team Lead yang resign setelah sebulan doang. Bahkan ada Product Manager yang baru 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 perusahaan ada yang bilang dokumentasi ga tertulis dengan baik. Mungkin ada benarnya. Seringkali deskripsi dari task itu kayak notes, cuma bullet points ga ada story. Sering juga saat ada bugs di tiketnya cuma ditulis “bugs di halaman x” tanpa dijelasin ini saat apa, bagaimana langkah-langkahnya, terjadi di client apa, dan sejenisnya. Dikira gw dukun kali ya bisa menebak bugsnya apa. Gw sendiri beberapa kali melaporkan ini tapi masih tetap aja begitu. Walau begitu, gw sering solved bugs yang lama ga ke-solved sama yang lain😎.
Pasca Probation
Pasca probation rencananya gw ingin resign aja. Naas, Covid-19 menyerang🦠. Pemerintah memerintahkan 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 secara manual dari HttpServletRequest. Contohnya seperti ini:
@RequestMapping("/list")
public ResponseEntity<?> getUserList(HttpServletRequest request) {
Map<String, String> paramMap = Utilities.constructParamMap(request);
UserContext context = new UserContext();
context.setType(paramMap.get("type"));
//logics
}Masing-masing parameter diekstrak ke dalam Map lewat utilities class, lalu mapping manual ke dalam objek Data. Validasi inputnya manual, padahal bisa pake Hibernate Validator. Di Spring sebenarnya bisa menggunakan Data class pada parameter atau menggunakan anotasi @RequestBody jika menggunakan request body. Codenya gw refactor jadi begini:
@RequestMapping("/list")
public ResponseEntity<?> getUserList(UserListRequest request) {
//logics
}
@RequestMapping("/create")
public ResponseEntity<?> createUser(@RequestBody UserCreationRequest request) {
//logics
}Lebih simple dan ini standar Spring. Entah kenapa dulu mereka ekstrak & mapping manual🫣. Sejak gw bikin begini, teman-teman lainnya pada ngikut.
Pemilihan Primary Key
Di perusahaan sebelumnya gw diajarkan untuk menggunakan Surrogate Key sebagai primary key menggunakan Sequential ID atau UUID. Candidate Key hanya untuk Unique Constraint. Di sini dulu Primary Key sebagian besar pake Candidate Key. Kelemahannya adalah ketika Candidate Key itu berupa Composite Key dan berelasi dengan tabel lain maka kita harus mereferensikan key-key tersebut ke tabel lain. Selain itu Candidate Key jadi ga tahan perubahan. Dulu ada fitur yang ID-nya digenerate otomatis oleh sistem. Kemudian 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 tidak 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 static method invoke(RequestData, ResponseClass). Masalahnya, itu cuma handle request menggunakan query parameter dengan GET doang🫣. Kemampuannya sangat terbatas. Codebasenya masih pake Java 8, jadi belum ada HttpClient seperti di Java 11. Semua parameter ditampung ke dalam HashMap sehingga kita harus ekstrak lagi dari request objek ke Map secara manual🫣. Gw berinisiatif membuat immutable RequestWrapper menggunakan Builder Design Pattern agar bisa menggunakan berbagai opsi request seperti http method, bentuk request, header, content type, dan sebagainya.
OrderRequest request = new OrderRequest();
Map<String, String> headers = new HashMap()<>;
RequestWrapper createOrder = RequestWrapper.post("createOrder").requestBody(request).build();
CreateOrderResponse createOrderResponse = createOrder.invokes(CreateOrderResponse.class);
RequestWrapper updateOrder = RequestWrapper.put("updateOrder").requestBody(request).contentType(ContentType.JSON).build();
UpdateOrderResponse updateOrderResponse = updateOrder.invokes(UpdateOrderResponse.class);
RequestWrapper listOrder = RequestWrapper.get("listOrder").queryParameter(request).headers(headers).build();
ListOrderResponse listOrderResponse = listOrder.invokes(ListOrderResponse.class);API akan dipanggil sesuai opsi yang dipilih. Di sini ga perlu ekstrak request objek karena dihandle pakai reflection API. Proses development jadi lebih cepat dan ini sekarang jadi standar baru.
Improve Utilities
Di sini ada modul yang berisi utilities semacam Apache Commons gitu. Contohnya saat konversi String ke int dengan default value biasanya codenya gini:
String str = "";
int num = StringUtils.isValidInt(str) ? Integer.parseInt(str) : 0;Padahal bisa dibikin simple dengan satu method doang. Gw improve jadi gini:
int num = StringUtils.toInt(str); //default value 0
int numWithDefault = StringUtils.toInt(str, 100); //default value 100
public static toInt(String input, int defaultVal){
try{
return Integer.parseInt(input);
} catch (Exception e){
return defaultVal;
}
}
public static toInt(String input){
return toInt(input, 0);
}
Gw juga banyak melakukan improvement terhadap utilities semacam ini yang sering dilakukan seperti konversi tipe data, validasi, dan banyak utils lainnya.
Constant Collection
Ketika ada case menggunakan constant Collection biasanya menggunakan ArrayList atau HashMap. Masalahnya, ArrayList & HashMap itu mutable, ga cocok jadi constant. Selain itu, ga semua hal 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:
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.
@RequestMapping("/create")
public ResponseEntity<?> createUser(UserCreationRequest request) {
try{
//logics
} catch(InvalidRequestException e){
//handle response invalid request
} catch(ConstraintViolationException e){
//handle response cpnstraint violation
} catch(Exception e){
//handle response server error
}
}
@RequestMapping("/update")
public ResponseEntity<?> updateUser(UserUpdateRequest request) {
try{
//logics
} catch(InvalidRequestException e){
//handle response invalid request
} catch(ConstraintViolationException e){
//handle response constraint violation
} catch(Exception e){
//handle response server error
}
}Di perusahaan sebelumnya, standarnya adalah Exception handling ini terpusat karena seragam tiap endpoint. Spring sudah ada fitur pakai anotasi @ControllerAdvice. Gw pun bikin Controller Advice di sini.
@ControllerAdvice
public class ExceptionHandlerController extends ResponseEntityExceptionHandler{
private static final Map<Class<? extends Exception>, ResponseStatus> STATUS_BY_EXCEPTION = CollectionUtils.mapOf(map -> map
.put(InvalidRequestException.class, ResponseStatus.INVALID_REQUEST)
.put(ConstraintViolationException.class, ResponseStatus.CONSTRAINT_VIOLATION)
//other mappings
);
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleException(Exception ex, HttpServletRequest request){
ResponseStatus status = STATUS_BY_EXCEPTION.getOrDefault(ex.getClass(), ResponseStatus.INTERNAL_SERVER_ERROR);
printLog(ex, request, status);
return buildResponseEntity(status, ex.getMessage());
}
}Setelah bikin Controller Advice, logic di Controller jadi lebih rapi.
@RequestMapping("/create")
public ResponseEntity<?> createUser(UserCreationRequest request) {
//logics
}
@RequestMapping("/update")
public ResponseEntity<?> updateUser(UserUpdateRequest request) {
//logics
}Ini sekarang jadi standar buat Exception handling Response Status di service ini.
Routing API
Ada satu legacy service di mana koordinasi routingnya ga lewat Spring Controller. Melainkan lewat static method berdasarkan path.
public static Handler getHandler(String path, Map<String, String> payload){
switch (action) {
case "listOrder":
return new ListOrderHandler(path);
}
}Misalkan ada API yang call ke endpoint /listOrder, maka akan diarahkan ke class ListOrderHandler. Semua endpoint di service itu kayak gini routingnya menuju service class. Masalahnya, ini jadi god class. Puncaknya, suatu kali service ini jadi ga bisa compile karena switch case yang kegendutan. Line-nya nyampe 6000+😱. Salah satu senior memindahkan beberapa handler dari switch case menggunakan HashMap. Untuk sementara waktu ini aman dan udah bisa di-compile lagi. Tapi tetap aja, codenya berantakan dan verbose karena masih god class. Kalau ini gw refactor pakai Spring Controller maka jadi ga backward-compatible. Gw inisiatif bikin anotasi untuk routing langsung dari service class.
@HandlerPath(path = "listOrder", httpMethod = HttpMethod.GET)
@EnumPayload(name = "status", type = OrderStatusEnum.class)
@BasePayload(name = "keyword", type = ParameterValueType.STRING)
@DatePayload(name = "fromDate", type = "yyyy-MM-dd")
@JsonPayload(name = "items", type = OrderItem.class)
public class ListOrderHandler {
//service class logic
}Dengan anotasi @HandlerPath gw menggunakan Event Listener dari Spring agar saat startup Spring mendaftarkan path /listOder dan objek ListOrderHandler ini ke HashMap yang dibikin senior sebelumnya. Udah ga perlu lagi nulis switch case yang panjang, semuanya kini tinggal ditulis langsung di service class. Sekarang inovasi gw ini jadi standar buat endpoint baru di service ini. Kemudian 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 menggunakan 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 khusus dokumentasi.
PDF Generator
Salah satu legacy gw yang gw tinggalkan adalah cara bikin PDF. Dulu, buat bikin PDF itu lewat API Itext-PDF. Rumit banget. Tiap bikin satu fitur PDF baru butuh 13 SP karena API-nya terlalu kompleks dan ga intuitif untuk desain. Puncaknya saat gw dapat task bikin invoice dengan desain yang rumit seperti kolom dengan garis putus-putus, ada kolom yang digabung, logo client, font yang beda, kolom tanda tangan yang bisa di-custom, dan sebagainya. Gw nyerah make API itu. Team Lead gw sempat ngasih saran pake Jasper soalnya tim sebelah pake itu. Jasper tetap ga memenuhi kebutuhan tersebut. Setelah keliling di stackoverflow akhirnya gw dapat ide buat bikin PDF dari HTML. Gw bikin modul baru yang berfungsi untuk integrasi antara objek Data, Thymeleaf untuk construct HTML, dan library untuk convert dari HTML ke PDF. Jadi dari objek Data kita perlu generate HTML dulu sebelum convert ke PDF. Secara teori memang jadi ada 3 layer dari Java -> HTML -> PDF. Sebelumnya dari Java langsung ke PDF. Tapi secara penggunaan ini lebih praktis karena dengan HTML kita bisa bikin desain dengan gampang. Story point untuk bikin PDF yang biasanya 13 SP jadi 3 atau 5 doang sekarang. Modul HTML PDF buatan gw ini sekarang juga jadi standar untuk fitur PDF😎.
@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 di-rollback karena tiap method bikin transaksi masing-masing. Kalau di Spring Data JPA kita disuguhkan anotasi @Transactional. Gw berinovasi buat bikin anotasi sendiri mirip @Transactional di Spring Data JPA. Kebetulan sebelumnya gw sempat mempelajari cara kerja anotasi ini. Jadi gw tinggal implementasikan aja. 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 implementasi AOP. Ga lupa juga ThreadLocal gw clear tiap selesai eksekusi. Sekarang method yang ada anotasi ini jadi atomic transaksinya. Gw tambahin juga properti isReadOnly untuk membedakan routing koneksi read only dan read & write. Awalnya sesederhana itu aja. Kemudian ada masalah lain, yaitu ketika transaksi gagal, misalnya karena locking, dia langsung rollback dan error. Ga ada resiliensi atau 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 ada masalah terkait Write Skew. 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😎. Contoh penggunaannya kayak gini:
@LegacyTransactional(isolation = IsolationLevel.SERIALIZABLE, lockTimeout = 1,
retry = @RetryTransaction(on = {LockAcquisitionException.class, StaleObjectStateException.class}, max = 3, delay = 2))
public Response updateOrder() {
//update logic
}
@LegacyTransactional(isReadOnly = true, isolation = IsolationLevel.REPEATABLE_READ)
public Response listOrder() {
//list logic
}Di service ini sekarang inovasi gw ini juga jadi standar untuk endpoint baru😎.
Numeric Pagination
Waktu itu di production tiba-tiba halaman list order 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 juga belum mau mengubah UI/UX. Waktu itu kita sepakati date range filternya cuma 31 hari aja maksimal.
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 pernah compile error. Tapi tetap aja god class, cepat atau lambat suatu saat bakal bermasalah. Gw inisiatif mengganti pendekatan ini menggunakan HashMap. Oh ya, modul ini ga pake Spring, jadi satu-satunya cara paling bersih yang kepikiran ya dengan menampungnya di HashMap. Masalah lainnya di modul ini adalah ga ada unit test. Class di modul ini banyak pake static method dan reuse code lewat inheritance, hampir ga ada dependency injection sehingga susah dibikin unit testnya🫣. Gw inisiatif membuat pendekatan baru. Class processor yang baru gw bikin dengan dependency injection. Reuse code gw ubah lewat composition. Gw bikin adapter untuk akses reuse code dari legacy processor dengan processor yang baru yang gw inject 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 serba 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 mapping 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 dan ga ada lagi git conflict saat ada orang lain commit entity baru secara bersamaan.
Message Queue Menggunakan Redis Stream
Sebelumnya di sini proses queue menggunakan SQS AWS 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🥹. Terakhir gw lihat ini udah di-merge ke production.
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 benar-benar belajar banyak tentang ini. Masalahnya, di modul legacy struktur modulnya ga sesuai Clean Architecture di mana Persistence dan Framework harusnya berada di layer terluar. Di sini layer Persistence berada di dalam layer Use Case. Sehingga banyak yang menyalahgunakan ini. 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 saat dibaca 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 sehingga ribet mockingnya. Untuk endpoint baru karena udah pakai Clean Code sih aman. Jadi gw cuma apply unit test di code baru aja. 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 baru yang menggunakan Framework Reactive, yaitu WebFlux. Singkatnya, ini menggunakan Event Loop seperti di JavaScript untuk eksekusi logic secara asynchronous sehingga lebih kecil resource yang dibutuhkan daripada menggunakan Thread biasa yang cukup boros memori. Paradigmanya menggunakan Functional Programming. Masalahnya, syntaxnya cukup verbose dan sulit dibaca. Menurut pandangan gw, secara code gw masih 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 menggunakan library Jsprit. Sebenarnya itu cukup lengkap fiturnya seperti multiple warehouse, window time pick up & delivery, material type, quantity limit, dan banyak lainnya. Masalahnya si 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. Padahal ada constraint window time di mana customer hanya bisa menerima barang di jam tertentu. Kita udah coba memenuhi permintaan custom mereka dengan menambahkan algoritma sendiri. Seringkali client ngerasa rute yang digenerate ga sesuai dengan ekspektasi mereka dan ngerasa rute versi mereka yang lebih baik. Padahal setelah dibandingkan masih lebih efisien rute yang digenerate oleh program. Kadang rute versi client itu sering human error dan ga sesuai constraint seperti window time atau material kendaraan. Gw cukup stres waktu ngerjain ini yang ga ada ujungnya🥴.
Begadang Tanpa Hasil
Salah satu momen yang cukup lucu buat gw adalah saat ada fitur urgent yang harus dirilis cepat untuk kebutuhan training. Ini adalah fitur lama yang sempat dihapus dan ingin diaktifkan lagi dengan lebih matang. Gw udah bikin fitur itu dan kelar 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 seharian dari pagi gw gabut. Ternyata ada beberapa hal tambahan yang dibutuhkan mobile. Gw pun develop fitur tambahan itu di 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 ga merespon apa-apa. Gw cek di server ga ada tanda-tanda request masuk. Udah pasti masalahnya di mobile. Pas dicek ternyata ada code yang di-hardcode di mobile agar hanya client tertentu yang bisa menggunakan fitur itu. Saat testing QA kebetulan menggunakan akun client yang di-hardcode itu sehingga ga ada masalah. Pada akhirnya training itu ditunda di lain hari. Percuma gw udah bela-belain begadang ternyata ga ada hasilnya🤣.
Kultur Baik
Secara kultur di sini cukup baik. Semuanya 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 juga cukup terbuka dengan pendapat dan masukan yang diberikan. Mereka cukup mengapresiasi kontribusi yang kita lakukan. Kultur seperti ini jadi salah satu hal 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. Ga lama kemudian Unpaid Leave dicabut dan gaji kembali normal. Setahun berikutnya setelah lebaran terjadi gelombang layoff pertama. Di periode ini gw masih aman. Kebijakan Unpaid Leave kembali diberlakukan. Beberapa client tidak melanjutkan kerjasama. Beberapa calon client dengan nama besar gagal bergabung. Sempat ada kabar ada 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 TK nunggak. Cuti bersama sekarang 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 dua tim engineering kecil yang tersisa. Kerjaan numpuk karena kurang resource sementara gaji semakin dipotong. Gw merasa kondisi ini udah benar-benar ga sehat.
Akhir Perjalanan
Selama 2 tahun terakhir gw ga melihat tanda-tanda perubahan yang lebih baik. Gw memang udah merperkirakan ini 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 ingin nyari peluang baru setelah lebaran dan udah mesan tiket kembali ke Jakarta jauh-jauh hari. Secara mental gw emang udah siap ini bakal terjadi. Sekarang gw masih berusaha mencari peluang baru di tengah kondisi job market yang lagi anyep. Inginnya sih gw kerja di tempat yang lebih stabil untuk jangka panjang. Semoga gw segera menemukannya🤲.
