When designing an API, how do you decide whether a function should be `suspend`, and what are the consequences of adding or removing the modifier?
answer
- suspend = may wait without blocking; pure logic stays non-suspend
- Adding/removing suspend is a breaking change (Continuation param)
- Interface suspend hides sync vs async from callers
- Make suspend functions main-safe via withContext
- suspend () -> T is a distinct function type
basics
~20 sMark a function suspend if it may need to wait (IO, delays, other suspend calls) without blocking. Adding or removing suspend is a breaking change to callers because it changes where the function can be called from.
solid answer
~50 sA function should be `suspend` when it performs asynchronous or potentially-waiting work that callers should not block a thread for — network/DB IO, delays, or composing other suspend functions. Don't make a function `suspend` just because it's used in coroutines; pure CPU/synchronous logic stays non-suspend so it's callable everywhere. `suspend` is part of the public contract: adding it forces all callers into coroutine context (a binary- and source-breaking change), and removing it can break overrides and callers relying on suspension. On interfaces, marking a method `suspend` lets implementations be async or sync transparently — useful for abstractions like repositories. Beware: a `suspend` function should be **main-safe** by its own choice of `withContext` for blocking work, or clearly document that the caller must dispatch appropriately. Also, you cannot pass a `suspend` function reference where a plain function type is expected; lambdas with a `suspend` receiver are a distinct type (`suspend () -> T`).
go deeper
Knows suspend is for functions that wait/do IO, not pure logic.
Can choose suspend vs non-suspend per function and knows it restricts call sites.
Treats suspend as a contract, ensures main-safety with withContext, and recognizes binary/source breakage.
Sets API conventions (main-safety, no lying suspend functions) and manages compatibility across a published library.
## The decision: should this be `suspend`? Make it `suspend` when: - It performs **IO or network** work the caller shouldn't block on. - It **delays** or waits on a timer/event. - It **calls other suspend functions** as part of its job (composition). - It's an **interface method** whose implementations may be async (e.g. a `UserRepository.find`), so you don't leak the sync/async choice into the contract. Keep it **non-suspend** when: - It's pure, synchronous CPU logic (parsing, mapping, arithmetic). Marking it `suspend` needlessly restricts callers to coroutine contexts and adds a continuation parameter for no benefit. ## `suspend` is part of the public contract Adding or removing `suspend` is a **breaking change**: - The compiler adds a hidden `Continuation` parameter to suspend functions, so the JVM signature changes — it's **binary-incompatible**. - Source-wise, **adding** `suspend` forces every call site into a coroutine; **removing** it can break overrides declared `suspend` in subclasses and callers that relied on suspension points. ```kotlin interface UserRepository { suspend fun find(id: Long): User? // contract allows async impls } class HttpUserRepository : UserRepository { override suspend fun find(id: Long): User? = withContext(Dispatchers.IO) { httpClient.get(id) } // main-safe } class InMemoryUserRepository : UserRepository { override suspend fun find(id: Long): User? = cache[id] // sync, still legal } ``` ## Main-safety A well-designed `suspend` function is **main-safe**: it can be called from any dispatcher (including `Dispatchers.Main`) without blocking. The function itself should `withContext(Dispatchers.IO)` around any blocking work, rather than forcing every caller to remember to dispatch. This is a common senior-level convention. ## Function types A suspending lambda has type `suspend () -> T`, distinct from `() -> T`. You cannot pass a plain lambda where a `suspend` function type is required without it being inferred as suspending, and vice versa — relevant when designing higher-order suspend APIs (e.g. `coroutineScope { }` takes a `suspend CoroutineScope.() -> T`). ## Anti-patterns - Making everything `suspend` "to be safe" — pollutes the API and forces coroutine contexts unnecessarily. - A `suspend` function that internally blocks a thread (`Thread.sleep`, blocking JDBC) on the caller's dispatcher without `withContext` — it looks non-blocking but isn't (a 'lying' suspend function). - Exposing callback-based async as non-suspend when a clean `suspend` wrapper would serve callers better.
- What is a 'lying' suspend function?One declared `suspend` but that blocks the calling thread internally (e.g. blocking JDBC or Thread.sleep without withContext). It breaks the non-blocking expectation and can starve the caller's dispatcher.
- Why is adding `suspend` to a published API a binary-breaking change?The compiler appends a hidden Continuation parameter, changing the JVM method signature, so previously compiled callers no longer link.
saying these in an interview costs you the question
- Marking pure synchronous functions suspend 'just in case'
- Treating adding/removing suspend as a safe non-breaking edit
- Writing suspend functions that block the thread internally
- Not considering main-safety responsibility
- Thinking suspend () -> T and () -> T are the same type