← Back to books

Chapter 1: Design Patterns & Kotlin Essentials

What Are Design Patterns?

Design patterns are proven solutions to recurring problems in software development. They represent best practices refined over decades by experienced developers. Rather than being specific code snippets to copy and paste, patterns are templates that guide you toward solving common architectural challenges.

The concept was popularized by the "Gang of Four" (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their 1994 book "Design Patterns: Elements of Reusable Object-Oriented Software." They cataloged 23 patterns divided into three categories: Creational, Structural, and Behavioral.

Why Do Interviewers Care About Design Patterns?

When interviewers ask about design patterns, they're evaluating several things:

  1. Problem Recognition: Can you identify when a situation calls for a known solution?
  2. Communication Skills: Can you articulate your design decisions using shared vocabulary?
  3. Experience Level: Have you encountered enough real-world problems to appreciate these solutions?
  4. Code Quality Mindset: Do you think about maintainability and scalability?

A junior developer might solve a problem with brute force. A senior developer recognizes the pattern and applies a proven solution that future team members will immediately understand.

The Three Pattern Categories

Creational Patterns

These patterns deal with object creation mechanisms. They abstract the instantiation process, making systems independent of how objects are created, composed, and represented.

Patterns covered in this book:

  • Singleton
  • Factory
  • Builder
  • Prototype

Structural Patterns

These patterns focus on how classes and objects are composed to form larger structures. They help ensure that when parts of a system change, the entire structure doesn't need to change.

Patterns covered in this book:

  • Adapter
  • Facade
  • Decorator
  • Proxy

Behavioral Patterns

These patterns characterize the ways in which classes or objects interact and distribute responsibility. They describe patterns of communication between objects.

Patterns covered in this book:

  • Observer
  • Strategy
  • Command
  • State
  • Template Method
  • Chain of Responsibility
  • Memento

Kotlin Features That Simplify Patterns

Kotlin provides language features that make implementing design patterns more concise than traditional Java implementations. Understanding these features is essential before diving into the patterns themselves.

The object Keyword (Singleton Made Easy)

In Java, implementing a thread-safe Singleton requires careful consideration of synchronization. Kotlin solves this with a single keyword:

// Kotlin's built-in Singleton
object Analytics {
    fun trackEvent(name: String) {
        // Implementation
    }
}

// Usage
Analytics.trackEvent("button_clicked")

The compiler guarantees thread-safe lazy initialization.

Sealed Classes (Type-Safe Hierarchies)

Sealed classes restrict class hierarchies, making them perfect for representing fixed sets of types like states or results:

sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

// Compiler enforces exhaustive when expressions
fun render(state: UiState<User>) {
    when (state) {
        is UiState.Loading -> showLoading()
        is UiState.Success -> showUser(state.data)
        is UiState.Error -> showError(state.message)
    }
}

Data Classes (Prototype Pattern Built-In)

Data classes automatically generate copy(), equals(), hashCode(), and toString():

data class User(
    val id: String,
    val name: String,
    val email: String
)

val original = User("1", "John", "john@email.com")
val modified = original.copy(name = "Jane")

Extension Functions (Lightweight Adapters)

Extension functions let you add functionality to existing classes without inheritance:

// Adapting a timestamp to a readable format
fun Long.toFormattedDate(): String {
    val sdf = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault())
    return sdf.format(Date(this))
}

// Usage
val timestamp = System.currentTimeMillis()
val readable = timestamp.toFormattedDate()

Higher-Order Functions (Strategy Pattern Simplified)

Functions as first-class citizens enable lightweight Strategy implementations:

// Instead of creating strategy interfaces and classes
fun processPayment(
    amount: Double,
    strategy: (Double) -> PaymentResult
): PaymentResult {
    return strategy(amount)
}

// Usage with lambdas
val result = processPayment(99.99) { amount ->
    // Credit card processing logic
    PaymentResult.Success(transactionId = "123")
}

Delegation with by Keyword

Kotlin's delegation simplifies Decorator and Proxy patterns:

interface Repository {
    fun getData(): List<String>
}

class CachingRepository(
    private val delegate: Repository
) : Repository by delegate {
    
    private var cache: List<String>? = null
    
    override fun getData(): List<String> {
        return cache ?: delegate.getData().also { cache = it }
    }
}

Lazy Initialization

The lazy delegate provides thread-safe lazy initialization:

class ExpensiveResource {
    val connection: DatabaseConnection by lazy {
        // Only created when first accessed
        DatabaseConnection.create()
    }
}

SOLID Principles: The Foundation

Before applying design patterns, understand the SOLID principles they're built upon.

Single Responsibility Principle (SRP)

A class should have only one reason to change.

// Bad: ViewModel doing too much
class UserViewModel : ViewModel() {
    fun loadUser() { /* ... */ }
    fun formatDate(timestamp: Long): String { /* ... */ }
    fun validateEmail(email: String): Boolean { /* ... */ }
    fun saveToDatabase(user: User) { /* ... */ }
}

// Good: Separated responsibilities
class UserViewModel(
    private val getUserUseCase: GetUserUseCase,
    private val dateFormatter: DateFormatter
) : ViewModel() {
    fun loadUser() {
        val user = getUserUseCase()
        // ...
    }
}

class DateFormatter {
    fun format(timestamp: Long): String { /* ... */ }
}

class EmailValidator {
    fun validate(email: String): Boolean { /* ... */ }
}

Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

// Using sealed classes for extensibility
sealed class PaymentMethod {
    abstract fun process(amount: Double): PaymentResult
}

class CreditCard(val number: String) : PaymentMethod() {
    override fun process(amount: Double) = /* ... */
}

class PayPal(val email: String) : PaymentMethod() {
    override fun process(amount: Double) = /* ... */
}

// Adding new payment method doesn't modify existing code
class Crypto(val walletAddress: String) : PaymentMethod() {
    override fun process(amount: Double) = /* ... */
}

Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of subclasses without breaking the application.

// Good: Both implementations honor the contract
interface ImageLoader {
    fun load(url: String): Bitmap
}

class CoilImageLoader : ImageLoader {
    override fun load(url: String): Bitmap {
        // Returns Bitmap as expected
    }
}

class GlideImageLoader : ImageLoader {
    override fun load(url: String): Bitmap {
        // Returns Bitmap as expected
    }
}

Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they don't use.

// Bad: Fat interface
interface UserActions {
    fun onClick()
    fun onLongClick()
    fun onSwipe()
    fun onDoubleTap()
}

// Good: Segregated interfaces
interface Clickable {
    fun onClick()
}

interface LongClickable {
    fun onLongClick()
}

// Implement only what you need
class SimpleButton : Clickable {
    override fun onClick() { /* ... */ }
}

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

// Bad: Direct dependency on concrete class
class UserRepository {
    private val database = RoomDatabase() // Concrete dependency
}

// Good: Depend on abstraction
class UserRepository(
    private val database: Database // Interface
) {
    // Can work with any Database implementation
}

interface Database {
    fun query(sql: String): List<Row>
}

How to Talk About Patterns in Interviews

When discussing patterns in interviews, follow this structure:

  1. Name the pattern: "This is a case where the Factory pattern works well."

  2. Explain the problem it solves: "We need to create different ViewModels based on the screen type, but we don't want the Fragment to know about all the concrete implementations."

  3. Describe the solution briefly: "The Factory encapsulates the creation logic, so the Fragment just asks for a ViewModel and gets the right one."

  4. Mention trade-offs: "It adds a level of indirection, but it makes testing easier and keeps the Fragment focused on its own responsibilities."

Avoid reciting textbook definitions. Interviewers want to see that you understand when and why to use a pattern, not that you memorized the Gang of Four book.

Chapter Summary

  • Design patterns are proven solutions to common software problems
  • Kotlin's language features (object, sealed classes, data classes, extension functions, delegation) simplify pattern implementation
  • SOLID principles form the foundation that patterns build upon
  • In interviews, focus on explaining the problem a pattern solves and when to use it

What's Next

In the following chapters, we'll explore each pattern with:

  • A general example to understand the concept
  • Android/Compose-specific implementation
  • Clear guidelines on when to use or avoid the pattern
  • Common interview questions and how to answer them

Let's begin with the Creational patterns, starting with the Singleton pattern in Chapter 2.