Kotlin Professional Certificate study materials
Kotlin Professional Certificate study resources

The nullable lookup that passed CI

Kotlin Professional Certificate · Put your knowledge to work in context.

All case studies

The nullable lookup that passed CI

A payments team migrated a Java InventoryClient to Kotlin. The Java method signature is Map<String, Integer> stockBySku() with no nullability annotations; the map values can be null for discontinued SKUs. The Kotlin caller does: val qty = inventory.stockBySku()[sku] then if (qty >= threshold) reserve(). Staging tests use a mocked map with 40 SKUs, all present and non-null, so CI is green. In production, SKU-4412 is discontinued, qty is a platform type Int!, the comparison unboxes a null, and the service throws NPE; 0.4% of 1.2M daily quote requests fail. The team proposes adding ?: 0, but a staff engineer objects because zero would silently allow reservations. They also ask why the compiler did not require a null check and whether Map<String, Int> at the Kotlin boundary would have caught this at compile time. The service runs Kotlin 2.0 on JVM 17 and the Java client cannot be changed this quarter.

1. Diagnose the production NPE. Why did the Kotlin compiler accept `qty >= threshold`, and what does the `Int!` notation mean at this call site?

Read the worked answer

stockBySku() is a Java method, so Kotlin sees the return type as a platform type. The map lookup has type Int! (more precisely Integer!): it may be treated as Int or Int?, and the compiler does not force a null check. The expression qty >= threshold needs a primitive int, so Kotlin unboxes the Integer. Unboxing null throws the NPE at the comparison. CI passed because the mocked map contained 40 non-null values; no test exercised a key whose value is null. A Kotlin Map<String, Int> boundary would not make nulls impossible at runtime-Java can still insert null through erasure-but it would force the adapter to handle the null before exposing the value to Kotlin callers. The real defect is an unvalidated interop boundary, not a missing !!. Int! is not a type you can declare; it is a flexible type used only for Java interop. The compiler is permissive because Java APIs often have undocumented non-null contracts; the cost is that the check moves to runtime.

2. The team proposes `val qty = inventory.stockBySku()[sku] ?: 0`. Evaluate that fix and propose a safer boundary adapter for this domain.

Read the worked answer

?: 0 is unsafe here: zero is a valid stock quantity with a different business meaning from 'discontinued'. It would also hide missing keys, because map[sku] returns null for both an absent key and a present key mapped to null. A safer adapter distinguishes the cases: fun stock(sku: String): Stock { val map = javaClient.stockBySku(); if (!map.containsKey(sku)) return Stock.Unknown; return when (val raw = map[sku]) { null -> Stock.Discontinued; else -> Stock.Available(raw) } }. This keeps platform types inside one file and gives callers an exhaustive sealed result. If reservation must fail loudly, use ?: throw InventoryUnavailable(sku) rather than requireNotNull, which produces a generic IllegalArgumentException and loses the SKU context. requireNotNull is fine for programmer errors, not expected business states. Add logging/metrics at the adapter for null values so the 0.4% failure rate is visible. The boundary should be tested with a real HashMap containing a null value; a Mockito stub returning a map of non-null values cannot prove the contract.

3. Write a test that would have caught this without depending on a mocking framework's default behavior.

Read the worked answer

Use a real Java map and test the adapter, not the mocking framework's defaults. For example: val backing = HashMap<String, Integer>(); backing["SKU-4412"] = null; val adapter = InventoryAdapter { backing }; assertEquals(Stock.Discontinued, adapter.stock("SKU-4412")). Also add a missing-key case: assertEquals(Stock.Unknown, adapter.stock("SKU-0000")). If you must test the legacy caller directly, assert the domain exception rather than an NPE: assertThrows<InventoryUnavailable> { quoteService.quote("SKU-4412") }. The key property is that no platform type escapes the adapter; you can enforce this by making the adapter's public return type Stock and keeping InventoryClient private. A compile-time check is stronger than a runtime test: a Kotlin function returning Map<String, Int> cannot be implemented by directly returning the Java map without handling nulls, because the compiler will require a conversion or check. Finally, remove the staging mock that always returns non-null values, or add a null-valued fixture to it.