Anti Patterns
Java Developer Migration Pitfalls
Using !! (Not-Null Assertion) Everywhere
Severity: Critical
Why Problematic: Defeats Kotlin’s null safety, converts compile-time safety to runtime crashes.
// ❌ Bad - !! everywhere
fun getUser(id: String): User {
return repository.findById(id)!! // NullPointerException at runtime
}
fun processUser(user: User?) {
println(user!!.name) // Crashes if null
sendEmail(user!!.email)
}
// ✅ Better - use safe operators
fun getUser(id: String): User? {
return repository.findById(id)
}
fun processUser(user: User?) {
user?.let {
println(it.name)
sendEmail(it.email)
}
}Context: Only use !! when you’re absolutely certain the value cannot be null and can document why.
Writing Java-Style Getters/Setters
Severity: Minor
Why Problematic: Kotlin properties provide this automatically.
// ❌ Bad - Java style
class User {
private var name: String = ""
fun getName(): String {
return name
}
fun setName(name: String) {
this.name = name
}
}
// ✅ Better - Kotlin properties
class User {
var name: String = ""
}
// ✅ With validation
class User {
var name: String = ""
set(value) {
require(value.isNotBlank()) { "Name cannot be blank" }
field = value
}
}Context: Use properties unless you need complex getter/setter logic.
Not Using Data Classes
Severity: Minor
Why Problematic: Manually implementing equals/hashCode/toString is error-prone.
// ❌ Bad - manual implementation
class User(val id: String, val name: String) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is User) return false
return id == other.id && name == other.name
}
override fun hashCode(): Int {
return 31 * id.hashCode() + name.hashCode()
}
override fun toString(): String {
return "User(id=$id, name=$name)"
}
}
// ✅ Better - data class
data class User(val id: String, val name: String)Context: Use data classes for data-holding objects unless you need custom behavior.
Null Safety Violations
Returning Null Instead of Empty Collections
Severity: Major
Why Problematic: Forces callers to null-check, increases complexity.
// ❌ Bad - null collection
fun getUsers(): List<User>? {
val users = database.query()
return users // Can be null
}
// Caller must check
val users = getUsers()
if (users != null) {
users.forEach { ... }
}
// ✅ Better - empty collection
fun getUsers(): List<User> {
return database.query() ?: emptyList()
}
// Caller can use directly
getUsers().forEach { ... }Context: Return empty collections instead of null unless null has specific meaning (e.g., “not fetched yet” vs “empty result”).
Platform Type Unchecked
Severity: Major
Why Problematic: Java interop can introduce null unexpectedly.
// ❌ Bad - assuming Java method never returns null
val name: String = javaApi.getUserName() // Might be null!
// ✅ Better - explicit nullability
val name: String? = javaApi.getUserName()
val safeName: String = name ?: "Unknown"
// ✅ Or defensive
val name: String = requireNotNull(javaApi.getUserName()) {
"API returned null username"
}Context: Always treat Java method returns as potentially null unless documented otherwise.
Late Init Overuse
Severity: Major
Why Problematic: lateinit bypasses null safety, can crash if accessed before initialization.
// ❌ Bad - lateinit for everything
class UserService {
lateinit var repository: UserRepository
lateinit var cache: Cache
lateinit var logger: Logger
}
// ✅ Better - constructor injection
class UserService(
private val repository: UserRepository,
private val cache: Cache,
private val logger: Logger
)
// ✅ Or nullable if truly optional
class Service {
private var optionalDependency: Dependency? = null
}Context: Only use lateinit for dependency injection frameworks where constructor injection isn’t possible.
Coroutine Misuse
Using runBlocking in Production
Severity: Critical
Why Problematic: Blocks threads, defeats purpose of coroutines.
// ❌ Bad - runBlocking in prod
fun getUser(id: String): User {
return runBlocking { // Blocks thread!
fetchUserSuspend(id)
}
}
// ✅ Better - make function suspend
suspend fun getUser(id: String): User {
return fetchUserSuspend(id)
}
// ✅ Or use proper scope
class UserService {
private val scope = CoroutineScope(Dispatchers.IO)
fun getUser(id: String, callback: (User) -> Unit) {
scope.launch {
val user = fetchUserSuspend(id)
withContext(Dispatchers.Main) {
callback(user)
}
}
}
}Context: runBlocking only for tests and main() functions.
GlobalScope for Everything
Severity: Critical
Why Problematic: Memory leaks, can’t cancel, violates structured concurrency.
// ❌ Bad - GlobalScope
fun loadData() {
GlobalScope.launch { // Leaks if view destroyed
val data = fetchData()
updateUI(data)
}
}
// ✅ Better - lifecycle-aware scope
class ViewModel {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
fun loadData() {
scope.launch {
val data = fetchData()
updateUI(data)
}
}
fun onCleared() {
scope.cancel()
}
}Context: Never use GlobalScope except for truly application-lifetime operations.
Not Using Dispatchers
Severity: Major
Why Problematic: Blocking operations on wrong dispatcher hurt performance.
// ❌ Bad - blocking on Default dispatcher
suspend fun fetchUser(id: String): User {
return httpClient.get("/users/$id") // Blocks coroutine
}
// ✅ Better - use IO dispatcher
suspend fun fetchUser(id: String): User {
return withContext(Dispatchers.IO) {
httpClient.get("/users/$id")
}
}
// ✅ CPU-bound work on Default
suspend fun processData(data: List<Int>): List<Int> {
return withContext(Dispatchers.Default) {
data.map { complexCalculation(it) }
}
}Context: IO for blocking I/O, Default for CPU-intensive work, Main for UI updates.
Performance Anti-Patterns
Sequence Misuse
Severity: Minor
Why Problematic: Sequences have overhead, not always faster.
// ❌ Bad - sequence for small collection
val result = listOf(1, 2, 3)
.asSequence() // Overhead not worth it
.map { it * 2 }
.toList()
// ✅ Better - direct operations
val result = listOf(1, 2, 3)
.map { it * 2 }
// ✅ Good - sequence for large collection
val result = (1..1_000_000).asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(100)
.toList()Context: Use sequences for large collections or when chaining many operations.
Unnecessary Object Creation in Loops
Severity: Major
Why Problematic: Excessive allocations cause garbage collection pressure.
// ❌ Bad - creates objects in loop
fun process(data: List<String>): List<Result> {
val results = mutableListOf<Result>()
for (item in data) {
results.add(Result(item.uppercase())) // New object each iteration
}
return results
}
// ✅ Better - functional approach
fun process(data: List<String>): List<Result> {
return data.map { Result(it.uppercase()) } // Still creates objects but clearer
}
// ✅ Best - inline class for zero overhead
@JvmInline
value class Result(val value: String)
fun process(data: List<String>): List<Result> {
return data.map { Result(it.uppercase()) }
}Context: Profile before optimizing, but be aware of allocation patterns.
Excessive Use of Reflection
Severity: Minor
Why Problematic: Reflection is slow and bypasses compile-time checks.
// ❌ Bad - reflection in hot path
fun process(obj: Any) {
val clazz = obj::class
val properties = clazz.memberProperties // Slow!
properties.forEach { prop ->
println("${prop.name}: ${prop.get(obj)}")
}
}
// ✅ Better - visitor pattern or sealed classes
sealed class Entity {
abstract fun process()
}
data class User(val name: String) : Entity() {
override fun process() {
println("User: $name")
}
}Context: Reflection for frameworks/serialization OK, but avoid in hot code paths.
Type System Misuse
Any Type Overuse
Severity: Major
Why Problematic: Loses type safety benefits.
// ❌ Bad - Any everywhere
fun process(data: Any): Any {
return when (data) {
is String -> data.uppercase()
is Int -> data * 2
else -> data
}
}
// ✅ Better - sealed class
sealed class Data {
data class Text(val value: String) : Data()
data class Number(val value: Int) : Data()
}
fun process(data: Data): Data {
return when (data) {
is Data.Text -> Data.Text(data.value.uppercase())
is Data.Number -> Data.Number(data.value * 2)
}
}
// ✅ Or generics
fun <T> process(data: T, transform: (T) -> T): T {
return transform(data)
}Context: Use Any only when truly heterogeneous types needed (JSON parsing, serialization).
Star Projection Everywhere
Severity: Minor
Why Problematic: Loses generic type information.
// ❌ Bad - star projection loses information
fun processList(list: List<*>) {
list.forEach {
// Can't do much with it: Any?
}
}
// ✅ Better - preserve type
fun <T> processList(list: List<T>, process: (T) -> Unit) {
list.forEach { process(it) }
}
// ✅ Or bounded type
fun <T : Number> sumList(list: List<T>): Double {
return list.sumOf { it.toDouble() }
}Context: Star projection when type doesn’t matter, but preserve types when possible.
Mutable vs Immutable Confusion
Severity: Major
Why Problematic: Unexpected mutations cause bugs.
// ❌ Bad - mutable when immutable intended
fun getUsers(): MutableList<User> {
return database.query().toMutableList()
}
// Caller can modify!
val users = getUsers()
users.clear() // Oops!
// ✅ Better - immutable interface
fun getUsers(): List<User> {
return database.query()
}
// ✅ Internal mutability, external immutability
class UserCache {
private val _users = mutableListOf<User>()
val users: List<User> get() = _users.toList()
fun add(user: User) {
_users.add(user)
}
}Context: Return immutable interfaces, use mutable collections internally.
Code Organization
God Classes
Severity: Major
Why Problematic: Violates Single Responsibility Principle, hard to test and maintain.
// ❌ Bad - does everything
class UserManager {
fun createUser() { }
fun deleteUser() { }
fun sendEmail() { }
fun processPayment() { }
fun generateReport() { }
fun validateInput() { }
fun logActivity() { }
// ... 50 more methods
}
// ✅ Better - single responsibilities
class UserService(
private val repository: UserRepository,
private val emailService: EmailService
) {
fun createUser(data: CreateUserRequest): User {
val user = data.toUser()
repository.save(user)
emailService.sendWelcome(user)
return user
}
}
class PaymentService { }
class ReportGenerator { }Context: Keep classes focused on one responsibility.
Public Everything
Severity: Minor
Why Problematic: Exposes implementation details, hard to refactor.
// ❌ Bad - everything public
class UserService {
val database: Database // Should be private!
val cache: Cache // Should be private!
fun validateEmail(email: String): Boolean { } // Should be private!
fun hashPassword(password: String): String { } // Should be private!
}
// ✅ Better - minimal public surface
class UserService(
private val database: Database,
private val cache: Cache
) {
fun createUser(name: String, email: String): User {
validateEmail(email) // Private helper
// ...
}
private fun validateEmail(email: String): Boolean {
return email.contains("@")
}
private fun hashPassword(password: String): String {
return BCrypt.hash(password)
}
}Context: Make everything private by default, expose only what’s necessary.
Testing Anti-Patterns
Testing Implementation Details
Severity: Major
Why Problematic: Tests break on refactoring, not on behavior changes.
// ❌ Bad - tests internal calls
@Test
fun `create user test`() {
service.createUser("Alice", "alice@example.com")
verify { repository.save(any()) } // Implementation detail
verify { cache.put(any(), any()) } // Implementation detail
verify { logger.info(any()) } // Implementation detail
}
// ✅ Better - tests behavior
@Test
fun `creating user makes it retrievable`() {
val userId = service.createUser("Alice", "alice@example.com")
val user = service.getUser(userId)
assertEquals("Alice", user.name)
assertEquals("alice@example.com", user.email)
}Context: Test public contracts, not private implementation.
No Assertions
Severity: Critical
Why Problematic: Test passes even if code is broken.
// ❌ Bad - no assertions
@Test
fun `test user creation`() {
service.createUser("Alice", "alice@example.com")
// Test passes but proves nothing!
}
// ✅ Better - clear assertions
@Test
fun `creating user returns user with generated ID`() {
val user = service.createUser("Alice", "alice@example.com")
assertNotNull(user.id)
assertEquals("Alice", user.name)
assertEquals("alice@example.com", user.email)
}Context: Every test needs at least one meaningful assertion.
Related Resources
Learn more:
- Best Practices - Idiomatic patterns
- Beginner Tutorial - Fundamentals
- Intermediate Tutorial - Production patterns
How-To Guides: