System Design Interviews in Android

System design interviews can be a daunting part of the interview process for Android Engineers. While the focus often leans towards architecture, performance, scalability, and reliability, many system design concepts are transferable to mobile development, especially when working with Kotlin and Jetpack Compose. In this article, we’ll explore 12 essential algorithms that play a pivotal role in system design, offering insights into how they can be used effectively in Android Kotlin Compose-based applications.


1. Bloom Filter: Reducing Costly Lookups

A simple example of a Bloom Filter to prevent unnecessary database or network lookups.

class BloomFilter(val size: Int, val hashFunctions: List<(String) -> Int>) {
    private val bitSet = BitSet(size)

    fun add(element: String) {
        hashFunctions.forEach {
            val hash = it(element) % size
            bitSet.set(hash)
        }
    }

    fun contains(element: String): Boolean {
        return hashFunctions.all {
            val hash = it(element) % size
            bitSet.get(hash)
        }
    }
}

// Usage Example
val bloomFilter = BloomFilter(
    size = 1000,
    hashFunctions = listOf(
        { input: String -> input.hashCode() },
        { input: String -> input.length.hashCode() }
    )
)

bloomFilter.add("John")
println(bloomFilter.contains("John")) // Should return true
println(bloomFilter.contains("Alice")) // Likely false

2. Geohash: Location-Based Services

Using Geohash for nearby locations.

// Example using Geohash library
import org.geohash.GeoHash
import com.google.android.gms.maps.model.LatLng

fun getNearbyGeohash(latitude: Double, longitude: Double): String {
    val geohash = GeoHash.withCharacterPrecision(latitude, longitude, 7)
    return geohash.toBase32()
}

val geohash = getNearbyGeohash(37.7749, -122.4194) // San Francisco
println("Geohash: $geohash")

3. Hyperloglog: Estimating Unique Elements

This can be implemented by tracking unique user IDs or events in a mobile app.

// Using Hyperloglog for tracking unique views
val uniqueUsers = mutableSetOf<String>()

fun addUniqueUser(userId: String) {
    uniqueUsers.add(userId)
}

fun getUniqueUserCount() = uniqueUsers.size

// Simulate adding users
addUniqueUser("user1")
addUniqueUser("user2")
addUniqueUser("user1")

println("Unique users: ${getUniqueUserCount()}")

4. Consistent Hashing: Efficient Data Distribution

A consistent hashing example to distribute tasks.

class ConsistentHashing(private val nodes: List<String>) {
    fun getNode(key: String): String {
        val hash = key.hashCode()
        val nodeIndex = Math.abs(hash % nodes.size)
        return nodes[nodeIndex]
    }
}

val nodes = listOf("Node A", "Node B", "Node C")
val consistentHash = ConsistentHashing(nodes)

println(consistentHash.getNode("user1"))  // It could print "Node B"

5. Merkle Tree: Verifying Data Integrity

Example of a Merkle Tree used for verifying data integrity.

data class MerkleNode(val hash: String, val left: MerkleNode? = null, val right: MerkleNode? = null)

fun createMerkleTree(data: List<String>): MerkleNode {
    if (data.size == 1) {
        return MerkleNode(data[0])
    }

    val mid = data.size / 2
    val left = createMerkleTree(data.subList(0, mid))
    val right = createMerkleTree(data.subList(mid, data.size))

    val combinedHash = (left.hash + right.hash).hashCode().toString()
    return MerkleNode(combinedHash, left, right)
}

val tree = createMerkleTree(listOf("A", "B", "C", "D"))
println("Root Hash: ${tree.hash}")

6. Raft Algorithm: Consensus in Distributed Databases

A simplified simulation of Raft’s consensus in Android.

// Simulate Raft leader election process
class RaftLeaderElection(val nodes: List<String>) {
    private var leader: String? = null

    fun electLeader(): String {
        leader = nodes.random()
        return leader!!
    }
}

val raft = RaftLeaderElection(listOf("Node A", "Node B", "Node C"))
println("Leader is: ${raft.electLeader()}")

7. Lossy Count: Estimating Item Frequencies

Using the Lossy Count algorithm to estimate frequencies of items.

class LossyCount(val threshold: Int) {
    private val counts = mutableMapOf<String, Int>()

    fun add(element: String) {
        counts[element] = counts.getOrDefault(element, 0) + 1
    }

    fun getFrequencies(): Map<String, Int> {
        return counts.filter { it.value >= threshold }
    }
}

val lossyCount = LossyCount(2)
lossyCount.add("Apple")
lossyCount.add("Apple")
lossyCount.add("Banana")

println(lossyCount.getFrequencies())  // Expected: {Apple=2}

8. QuadTree: Spatial Partitioning

A basic implementation of QuadTree for location-based services.

class QuadTree(val boundary: Rect, val capacity: Int) {
    private val points = mutableListOf<LatLng>()
    private var divided = false

    fun insert(point: LatLng): Boolean {
        if (!boundary.contains(point)) return false
        if (points.size < capacity) {
            points.add(point)
            return true
        }
        if (!divided) {
            subdivide()
        }
        // Insert into the appropriate quadrant
        return true
    }

    private fun subdivide() {
        divided = true
        // Divide into 4 quadrants
    }
}

data class LatLng(val latitude: Double, val longitude: Double)
data class Rect(val latMin: Double, val latMax: Double, val lonMin: Double, val lonMax: Double) {
    fun contains(point: LatLng) = point.latitude in latMin..latMax && point.longitude in lonMin..lonMax
}

val rect = Rect(37.0, 38.0, -122.5, -123.0)
val quadTree = QuadTree(rect, 2)
val point = LatLng(37.7749, -122.4194)

quadTree.insert(point)

9. Operational Transformation: Real-Time Collaboration

Basic collaboration on shared data.

// Simulate real-time text collaboration
class OperationalTransformation {
    var document = StringBuilder()

    fun applyOperation(op: String) {
        document.append(op)
    }

    fun getDocument() = document.toString()
}

val ot = OperationalTransformation()
ot.applyOperation("Hello ")
ot.applyOperation("World!")

println("Document: ${ot.getDocument()}")

10. Leaky Bucket: Rate Limiting in APIs

Simple Leaky Bucket algorithm for controlling API rate limits.

class LeakyBucket(val capacity: Int, val leakRate: Int) {
    private var waterLevel = 0

    fun addRequest() {
        if (waterLevel < capacity) {
            waterLevel++
            println("Request added. Water level: $waterLevel")
        } else {
            println("Bucket full, try again later.")
        }
    }

    fun leak() {
        if (waterLevel > 0) {
            waterLevel -= leakRate
        }
    }
}

val bucket = LeakyBucket(capacity = 5, leakRate = 1)

bucket.addRequest()  // Should succeed
bucket.addRequest()  // Should succeed
bucket.leak()  // Leaks 1 unit

11. Rsync: File Synchronization

Simplified rsync simulation for syncing files.

fun syncFiles(source: String, destination: String) {
    println("Syncing files from $source to $destination")
    // Simulate file sync
}

syncFiles("localFile", "remoteServer")

12. Ray Casting: Collision Detection

A basic example for collision detection in Android.

// Simulate ray casting for collision detection in 2D space
fun isCollision(ray: Line, objectShape: Rect): Boolean {
    return ray.intersects(objectShape)
}

data class Line(val start: Point, val end: Point) {
    fun intersects(rect: Rect): Boolean {
        // Logic to check if the line intersects the rectangle
        return true
    }
}

data class Point(val x: Int, val y: Int)
data class Rect(val x: Int, val y: Int, val width: Int, val height: Int)

val ray = Line(Point(0, 0), Point(5, 5))
val rect = Rect(2, 2, 2, 2)

println(isCollision(ray, rect))  // Will print true if there's a collision

Each of these algorithms can be adapted to Android Kotlin Compose for efficient, scalable applications, enabling you to optimize performance and user experience.

πŸ“’ Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! 😊 please leave a comment below. I’d love to hear from you! πŸ‘‡

Happy coding! πŸ’»✨

Implementing an LRU Cache in Android using Kotlin and Jetpack Compose

In Android development, caching is a critical technique for storing data locally for quicker access, reducing network calls, and enhancing the user experience. One popular caching technique is the Least Recently Used (LRU) cache, which automatically evicts the least recently accessed data when the cache reaches its limit. This post explores how to implement an LRU cache in Android using Kotlin and Jetpack Compose.


What is an LRU Cache?

Least Recently Used (LRU) is a caching strategy that removes the least recently accessed items first when the cache exceeds its defined limit. It's optimal when you want to cache a limited amount of data, such as images, objects, or API responses, and ensure only frequently used items remain in memory.

Benefits:

  • Reduces memory footprint

  • Speeds up data retrieval

  • Avoids unnecessary API calls or file reads

  • Automatically manages cache eviction


Use Cases of LRU Cache in Android

Use Case Description
Image Caching Store bitmap images fetched from the network to avoid repeated downloads
Network Responses Save HTTP responses (e.g., user profile, dashboard data) to speed up load times
File or Object Caching Cache local files or data models in memory for quick reuse
Custom In-App Caching Temporary store for autocomplete suggestions, search history, etc.

 Implementing LRU Cache in Kotlin (Without External Libraries)

Step 1: Create an LRU Cache Manager

class LruCacheManager<K, V>(maxSize: Int) {
    private val cache = object : LruCache<K, V>(maxSize) {
        override fun sizeOf(key: K, value: V): Int {
            return 1 // Customize this if needed (e.g., for Bitmaps)
        }
    }

    fun put(key: K, value: V) {
        cache.put(key, value)
    }

    fun get(key: K): V? {
        return cache.get(key)
    }

    fun evictAll() {
        cache.evictAll()
    }
}

☝️ Note: This uses Android’s built-in LruCache<K, V> from android.util.LruCache.


Example: Caching User Profiles

Let’s say you’re loading user profiles from an API, and want to cache them in memory to avoid reloading them repeatedly.

Step 2: Define a simple data model

data class UserProfile(val id: Int, val name: String, val avatarUrl: String)

Step 3: Create a ViewModel using StateFlow and LruCache

class UserProfileViewModel : ViewModel() {
    private val cache = LruCacheManager<Int, UserProfile>(maxSize = 5)

    private val _userProfile = MutableStateFlow<UserProfile?>(null)
    val userProfile: StateFlow<UserProfile?> = _userProfile

    fun loadUserProfile(userId: Int) {
        cache.get(userId)?.let {
            _userProfile.value = it
            return
        }

        viewModelScope.launch {
            // Simulate network call
            delay(1000)
            val user = UserProfile(userId, "User $userId", "https://picsum.photos/200/200?random=$userId")
            cache.put(userId, user)
            _userProfile.value = user
        }
    }
}

Jetpack Compose UI

@Composable
fun UserProfileScreen(viewModel: UserProfileViewModel = viewModel()) {
    val profile by viewModel.userProfile.collectAsState()

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = { viewModel.loadUserProfile((1..10).random()) }) {
            Text("Load Random User")
        }

        Spacer(modifier = Modifier.height(20.dp))

        profile?.let {
            Text(text = it.name, style = MaterialTheme.typography.titleLarge)
            AsyncImage(
                model = it.avatarUrl,
                contentDescription = "Avatar",
                modifier = Modifier.size(120.dp).clip(CircleShape)
            )
        } ?: Text("No user loaded")
    }
}

Best Practices for Using LRU Cache

Tip Description
Use LruCache only for in-memory caching Do not use it for persistent or disk-based caching
Define a sensible maxSize Based on app usage pattern and available memory
For image caching, use Bitmap size as the unit Override sizeOf() method
Evict cache on memory warnings Use onTrimMemory() or onLowMemory() callbacks
Use libraries like Coil, Glide, or Picasso They offer built-in LRU support for image loading

πŸ”š Remember

The LRU caching mechanism is a simple yet powerful technique in Android development. It keeps your UI snappy and responsive by reducing network usage and data load time. While libraries like Glide manage LRU for images out of the box, creating your custom LRU cache gives you flexibility and control—especially when caching JSON responses or domain objects.

Pro Tip: Combine LruCache with StateFlow and Jetpack Compose for real-time UI updates and smoother UX.



#AndroidDevelopment #Kotlin #JetpackCompose #LRUCache #CachingInAndroid #StateFlow #PerformanceOptimization



πŸ“’ Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! 😊 please leave a comment below. I’d love to hear from you! πŸ‘‡

Happy coding! πŸ’»✨



Cracking the Senior Android Engineer Interview: What to Expect from Start to Finish

Stepping into the interview process for a Senior Android Engineer role can be both exciting and challenging. Whether you’re preparing for your dream job at a big tech firm or a promising startup, understanding the structure and topics across each interview stage is crucial.

In this blog, we’ll break down the commonly asked topics from the initial recruiter round to the final bar-raiser interview, including everything from Kotlin coding to system design and behavioral assessments.


 Initial HR/Recruiter Screening

This is mostly a soft round to gauge your fit for the role and company culture.

Topics:

  • Summary of your Android development experience

  • Key projects you've worked on (apps, user base, challenges)

  • Current role, notice period, and salary expectations

  • Why you're exploring new opportunities

  • Communication skills and professional demeanor


Online Technical Coding Round

Here the focus is on core problem-solving skills using Kotlin.

Topics to Prepare:

  • Data Structures & Algorithms:

    • Arrays, LinkedLists, Trees, Graphs, HashMaps

    • Sorting, searching, recursion, backtracking

  • Kotlin-specific Concepts:

    • Coroutines (Job, SupervisorJob, Dispatchers)

    • Flow, StateFlow, and Channels

    • Lambda expressions, extension functions, and delegates

  • Concurrency & Asynchronous Programming in Android

Tools: HackerRank, Codility, or take-home assignments


System Design Interview

This round evaluates how you architect scalable, modular Android apps.

Key Focus Areas:

  • Clean Architecture (Presentation → Domain → Data layers)

  • MVVM vs MVI vs MVP — and when to choose which

  • Real-world design:

    • Offline-first apps

    • Syncing with API and caching (Room, DataStore)

    • Push notifications & background sync (WorkManager)

  • Dependency Injection (Hilt/Dagger2)

  • Multi-module project structuring

Example: “Design a banking app with authentication, balance display, and transaction history”


Android Platform & Jetpack Deep Dive

Here, expect questions on Jetpack libraries, Compose UI, and platform internals.

Topics:

  • Jetpack Compose:

    • State management

    • Recomposition and performance pitfalls

  • Lifecycle Management:

    • ViewModel, LifecycleOwner, LifecycleObserver

  • Jetpack Libraries:

    • Navigation, Room, Paging, WorkManager, DataStore

  • Security:

    • Encrypted storage, biometric authentication, Keystore

  • Accessibility:

    • Compose semantics, TalkBack support, content descriptions


Testing & Debugging

A great Senior Android Engineer writes testable and maintainable code.

What You Should Know:

  • Unit Testing with JUnit, Mockito, MockK

  • UI Testing with Espresso and Compose testing APIs

  • Integration Testing with HiltTestApplication

  • Debugging ANRs, memory leaks (LeakCanary), performance bottlenecks

  • Using tools like Crashlytics, Logcat, StrictMode


CI/CD, DevOps, and Release Management

Modern Android teams value automation and fast feedback cycles.

Topics:

  • CI/CD tools: Jenkins, GitHub Actions, Bitrise

  • Gradle optimization, build flavors, product types

  • Feature flag implementation (Gradle + Firebase Remote Config)

  • Code quality enforcement: Detekt, Lint, SonarQube

  • Secure and efficient release strategies (Play Store, Firebase App Distribution)


Behavioral & Leadership Assessment

This round checks for team collaboration, mentorship, and decision-making skills.

Example Questions:

  • Tell us about a time you led a project or mentored a junior

  • How do you resolve disagreements with product or design teams?

  • What’s your strategy for balancing tech debt vs. feature delivery?

  • How do you stay updated with evolving Android trends?

Tip: Use the STAR method (Situation, Task, Action, Result) to answer.


Bar-Raiser or VP/CTO Round

This is the make-or-break round for many companies.

Focus Areas:

  • End-to-end ownership of features and impact

  • Trade-offs made during architecture decisions

  • Innovation, optimization, or cost-saving initiatives you've led

  • Long-term vision, technical leadership, and culture fit


πŸ”š Final Thoughts

Landing a Senior Android Engineer role isn’t just about writing great Kotlin code. It’s about demonstrating architectural mastery, leadership, and a product-first mindset across every round.

Start prepping smart by:

  • Practicing DSA in Kotlin

  • Building or refactoring a multi-module Compose app

  • Designing systems (like a chat app or e-commerce app)

  • Writing testable, clean code

  • Staying up to date with Jetpack and security best practices


What Next?

Want mock interview questions, detailed Kotlin exercises, or a full Android app architecture walkthrough? Drop a comment or subscribe for more deep-dives every week.


πŸ”— Related Reads:



πŸ“’ Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! 😊 please leave a comment below. I’d love to hear from you! πŸ‘‡

Happy coding! πŸ’»✨

Exploring Scope Functions in Kotlin Compose

Kotlin provides several powerful scope functions that make your code more readable and concise. These scope functions — let, run, apply, also, and with — are particularly useful when working with Kotlin's modern Android development framework, Jetpack Compose. 

Scope functions in Kotlin allow you to execute a block of code within the context of an object. Each of these functions has different characteristics and return values. They can be used for various purposes, like modifying an object, chaining function calls, or simplifying code readability.

1. let – Transform the Object and Return the Result

The let function executes a block of code on the object it is invoked on and returns the result of the block. It's often used when working with an object and returning a result without modifying the original object.

Syntax:

val result = object.let {
    // Do something with object
    "result"
}

Example in Compose: Suppose we want to handle an event in Jetpack Compose like a button click, where we only need to transform the result or pass it on to another function:

val buttonText = "Click me"
val result = buttonText.let {
    it.toUpperCase()
}

Text(text = result)  // Output: "CLICK ME"

In this example, the let function transforms buttonText to uppercase without modifying the original string.

2. run – Execute a Block and Return the Result

run is similar to let, but it is typically used when you want to execute a block of code and return a result. Unlike let, run doesn’t take the object as an argument — instead, it works within the object's context.

Syntax:

val result = object.run {
    // Do something with object
    "result"
}

Example in Compose: When creating a composable, you can use run to set up complex UI elements or operations:

val result = "Hello".run {
    val length = length
    "Length of text: $length"
}

Text(text = result)  // Output: "Length of text: 5"

Here, we used run to access properties and perform operations on a string. The function directly returns the result without modifying the object.

3. apply – Configure an Object and Return It

The apply function is used when you want to modify an object and return the modified object itself. It’s particularly useful for setting multiple properties on an object.

Syntax:

val modifiedObject = object.apply {
    // Modify object properties
}

Example in Compose: For instance, in Jetpack Compose, you can use apply when configuring a Modifier object to add multiple modifications:

val modifier = Modifier
    .padding(16.dp)
    .apply {
        background(Color.Blue)
        fillMaxSize()
    }

Box(modifier = modifier) {
    Text(text = "Hello World")
}

In this case, the apply function allows chaining multiple properties on the Modifier object and returns the same object after applying changes.

4. also – Perform an Action and Return Object

The also function is often used when you want to perform additional actions on an object but don’t want to change or return a new object. It’s often used for logging or debugging.

Syntax:

val objectWithAction = object.also {
    // Perform actions like logging
}

Example in Compose: Suppose you want to log a value when a user clicks a button in Compose:

val clickCount = remember { mutableStateOf(0) }

Button(onClick = {
    clickCount.value = clickCount.value.also {
        println("Button clicked: ${clickCount.value} times")
    } + 1
}) {
    Text("Click Me")
}

In this example, the also function is used to log the click count before updating the value of clickCount.

5. with – Execute a Block on an Object Without Returning It

The with function is used when you want to perform several actions on an object without modifying or returning it. It operates similarly to run, but unlike run, which operates in the context of the object, with requires the object to be passed explicitly.

Syntax:

with(object) {
    // Perform actions
}

Example in Compose: If you want to configure a composable’s properties, you can use with for better readability:

val modifier = with(Modifier) {
    padding(16.dp)
    background(Color.Green)
    fillMaxSize()
}

Box(modifier = modifier) {
    Text("Welcome to Compose!")
}

Here, with is used to apply multiple modifiers without repeatedly referencing the Modifier object.

Key Differences Between Scope Functions

  • let: Useful when you transform the object and return a result. The object is passed as an argument to the block.

  • run: This is similar to let, but the object is accessed directly within the block. It is helpful when returning a result after performing operations.

  • apply: Modifies the object and returns the object itself. Ideal for object configuration.

  • also: Similar to apply, but used primarily for performing side actions (like logging), while returning the original object.

  • with: Works like run, but requires the object to be passed explicitly and is used when you need to operate on an object without modifying it.

When to Use Each in Jetpack Compose

  • let: When you must transform or pass an object’s value.

  • run: When performing operations within the context of an object and returning the result.

  • apply: When you need to modify an object (like a Modifier) and return it after changes.

  • also: For performing additional actions (e.g., logging or debugging) without changing the object.

  • with: When you want to execute multiple operations on an object without modifying it.

Summary

Scope functions are essential to Kotlin’s functional programming style, offering a concise and readable way to work with objects. In Jetpack Compose, they help streamline UI development, manage states, and enhance overall code readability. Whether you’re configuring UI elements, performing transformations, or logging actions, scope functions can significantly reduce boilerplate and improve the efficiency of your Kotlin Compose code.

πŸ“’ Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! 😊 please leave a comment below. I’d love to hear from you! πŸ‘‡

Happy coding! πŸ’»✨