Network Layer Architecture for Mobile Apps
I once shipped a release where the app crashed every time a user opened it on a flaky Wi-Fi connection. The root cause? A networking call buried inside a view controller that assumed the server would always respond within two seconds. No timeout handling. No retry. No error state in the UI. Just a force-unwrapped optional and a prayer.
That bug cost us three days of firefighting and a spike in one-star reviews. It also taught me something I now tell every candidate I interview: the network layer is the most important piece of infrastructure in your mobile app, and most teams get it wrong.
In a mobile system design interview, you will be asked to design features that depend on network calls — feeds, messaging, payments, search. How you structure the networking code underneath those features tells the interviewer whether you've built production apps or just followed tutorials.
Let's build a network layer from scratch, for both iOS and Android.
Why structure matters#
Open any legacy mobile codebase and you'll find the same pattern: URLSession.shared.dataTask or OkHttpClient().newCall() scattered across dozens of files. Each call site handles errors differently. Some retry, some don't. Auth headers get added in some places and forgotten in others. Testing is impossible without hitting real servers.
This isn't a hypothetical. I've seen this at startups and at companies with hundreds of engineers. The fix is always the same: centralize your networking into a single, well-defined layer.
A structured network layer gives you:
- One place to add auth headers instead of remembering to do it in every call
- Consistent error handling so the UI always knows what went wrong
- Testability because you can swap the real HTTP client for a mock
- Observability through centralized logging and metrics
- Resilience via retries, timeouts, and circuit breakers configured once
Core components#
A good mobile network layer has five parts. You don't need a framework for any of them — just clear boundaries.
API Client — the single entry point for all network requests. It owns the HTTP client (URLSession or OkHttpClient), applies shared configuration, and returns typed responses.
Request Builder — constructs URL requests from high-level descriptions. Takes an endpoint enum or object and produces a fully formed HTTP request with path, method, headers, query parameters, and body.
Response Parser — decodes raw data into domain models. Handles the happy path (valid JSON) and the sad path (malformed responses, unexpected schemas).
Error Handler — transforms HTTP status codes, network failures, and parsing errors into a single error type your app understands.
Interceptors / Middleware — a chain of processors that modify requests before they go out (add headers, log payloads) or modify responses when they come back (refresh tokens, retry on failure).
Let me show you how these fit together.
Wrapping the platform networking stack#
The HTTP Client#
On iOS, URLSession is powerful, but its raw API encourages scattered, callback-based code. The goal is a thin wrapper that gives you async/await, typed responses, and a single configuration point. On Android, OkHttp handles the HTTP transport and Retrofit generates type-safe API interfaces from annotations. The pattern is the same on both platforms: centralize configuration, expose a clean interface.
// Define your API as an interface
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: String): UserResponse
@POST("posts")
suspend fun createPost(@Body request: CreatePostRequest): PostResponse
@GET("feed")
suspend fun getFeed(
@Query("page") page: Int,
@Query("limit") limit: Int = 20
): FeedResponse
}
// Build the client with shared configuration
object NetworkModule {
fun provideApiService(
baseUrl: String,
tokenProvider: TokenProvider
): ApiService {
val client = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(tokenProvider))
.addInterceptor(LoggingInterceptor())
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
isLenient = true
}
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
.create(ApiService::class.java)
}
}
protocol NetworkClient {
func execute<T: Decodable>(_ request: APIRequest) async throws -> T
}
struct APIRequest {
let path: String
let method: HTTPMethod
let headers: [String: String]
let queryItems: [URLQueryItem]?
let body: Data?
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
}
final class URLSessionNetworkClient: NetworkClient {
private let session: URLSession
private let baseURL: URL
private let decoder: JSONDecoder
private let interceptors: [RequestInterceptor]
init(
baseURL: URL,
session: URLSession = .shared,
decoder: JSONDecoder = .init(),
interceptors: [RequestInterceptor] = []
) {
self.baseURL = baseURL
self.session = session
self.decoder = decoder
self.interceptors = interceptors
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder.dateDecodingStrategy = .iso8601
}
func execute<T: Decodable>(_ request: APIRequest) async throws -> T {
var urlRequest = try buildURLRequest(from: request)
// Run through interceptor chain
for interceptor in interceptors {
urlRequest = try await interceptor.intercept(urlRequest)
}
let (data, response) = try await session.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw try mapServerError(statusCode: httpResponse.statusCode, data: data)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw NetworkError.decodingFailed(error)
}
}
private func buildURLRequest(from request: APIRequest) throws -> URLRequest {
var components = URLComponents(url: baseURL.appendingPathComponent(request.path), resolvingAgainstBaseURL: false)
components?.queryItems = request.queryItems
guard let url = components?.url else {
throw NetworkError.invalidURL
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = request.method.rawValue
urlRequest.httpBody = request.body
urlRequest.timeoutInterval = 30
for (key, value) in request.headers {
urlRequest.setValue(value, forHTTPHeaderField: key)
}
return urlRequest
}
}
Notice what the Swift wrapper gives you: a protocol (NetworkClient) that your app depends on, and a concrete implementation that you can replace during testing. The interceptor chain is where auth, logging, and retry logic live — we'll get to that. The Kotlin version gets the equivalent seams from OkHttp's interceptor list and Retrofit's interface-based API definitions.
Interview Tip: Don't waste time explaining what Retrofit is. Interviewers know it. Instead, talk about what you configure — timeouts, interceptors, serialization strategy — and why. That's where the signal is.
Request and response modeling#
This is where most tutorials stop and most production bugs start. The server sends JSON. You decode it into a model. But what happens when the server adds a new field? Removes one? Changes a type from String to Int?
Defensive decoding#
Swift's Codable protocol and Kotlinx Serialization both handle most of this well, but you need defensive defaults.
@Serializable
data class UserResponse(
val id: String,
val name: String,
val email: String,
@SerialName("avatar_url")
val avatarUrl: String? = null,
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
val role: UserRole = UserRole.UNKNOWN
)
@Serializable
enum class UserRole {
@SerialName("member") MEMBER,
@SerialName("admin") ADMIN,
UNKNOWN;
}
@Serializable
data class PaginatedResponse<T>(
val data: List<T>,
val page: Int,
@SerialName("total_pages")
val totalPages: Int,
@SerialName("has_more")
val hasMore: Boolean
)
struct UserResponse: Decodable {
let id: String
let name: String
let email: String
let avatarUrl: String? // Nullable — the server may not return this
let createdAt: Date
let role: UserRole
enum UserRole: String, Decodable {
case member
case admin
case unknown
// Default to unknown for values the client doesn't recognize yet
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(String.self)
self = UserRole(rawValue: rawValue) ?? .unknown
}
}
}
// Wrapper for paginated responses
struct PaginatedResponse<T: Decodable>: Decodable {
let data: [T]
let page: Int
let totalPages: Int
let hasMore: Bool
}
Two things to note. First, the UserRole enum has an unknown case — in Swift via a custom initializer that falls back to .unknown, in Kotlin via the UserRole.UNKNOWN default — and this prevents a crash when the backend team adds a new role before the next app release ships. Second, the PaginatedResponse is generic, so you write the pagination boilerplate once.
On Android, the ignoreUnknownKeys = true setting in the Json builder (from the Retrofit setup above) is not optional. Without it, a single new field from the server will crash your entire app. I've seen this happen in production more than once.
Interview Tip: Mention forward compatibility in your interview. Say something like: "I always configure the decoder to ignore unknown keys, and I use default values for new enum cases, so the app doesn't break when the API evolves."
Error handling strategy#
Every network call can fail in three distinct ways, and your app needs to tell them apart:
- Network errors — the device is offline, the connection timed out, DNS failed
- Server errors — the server responded, but with a 4xx or 5xx status code
- Parsing errors — the response arrived, but the data doesn't match the expected shape
Here's how to model this as a type hierarchy.
sealed class NetworkError : Exception() {
// Network-level
data object NoConnection : NetworkError()
data object Timeout : NetworkError()
// Server-level
data object Unauthorized : NetworkError()
data object Forbidden : NetworkError()
data object NotFound : NetworkError()
data class RateLimited(val retryAfterSeconds: Int?) : NetworkError()
data class ServerError(val statusCode: Int, val serverMessage: String?) : NetworkError()
// Parsing
data class DecodingError(val cause: Throwable) : NetworkError()
val isRetryable: Boolean
get() = this is Timeout || this is ServerError || this is RateLimited
val userMessage: String
get() = when (this) {
is NoConnection -> "You appear to be offline. Check your connection and try again."
is Timeout -> "The request timed out. Please try again."
is Unauthorized -> "Your session has expired. Please sign in again."
is RateLimited -> "Too many requests. Please wait a moment."
is ServerError -> "Something went wrong on our end. Please try again later."
else -> "An unexpected error occurred."
}
}
enum NetworkError: Error {
// Network-level failures
case noConnection
case timeout
case invalidURL
case invalidResponse
// Server-level failures
case unauthorized // 401
case forbidden // 403
case notFound // 404
case rateLimited(retryAfter: Int?) // 429
case serverError(statusCode: Int, message: String?) // 5xx
// Parsing failures
case decodingFailed(Error)
var isRetryable: Bool {
switch self {
case .timeout, .serverError, .rateLimited:
return true
case .noConnection, .unauthorized, .forbidden,
.notFound, .invalidURL, .invalidResponse, .decodingFailed:
return false
}
}
var userMessage: String {
switch self {
case .noConnection:
return "You appear to be offline. Check your connection and try again."
case .timeout:
return "The request timed out. Please try again."
case .unauthorized:
return "Your session has expired. Please sign in again."
case .rateLimited:
return "Too many requests. Please wait a moment."
case .serverError:
return "Something went wrong on our end. Please try again later."
default:
return "An unexpected error occurred."
}
}
}
func mapServerError(statusCode: Int, data: Data) throws -> NetworkError {
let serverMessage = try? JSONDecoder().decode(ServerErrorBody.self, from: data).message
switch statusCode {
case 401: return .unauthorized
case 403: return .forbidden
case 404: return .notFound
case 429: return .rateLimited(retryAfter: nil)
case 500...599: return .serverError(statusCode: statusCode, message: serverMessage)
default: return .serverError(statusCode: statusCode, message: serverMessage)
}
}
The isRetryable property — present in both the Swift enum and the Kotlin sealed class — is doing real work here. When you build a retry mechanism later, you check this flag instead of scattering retry logic across the codebase.
Authentication and token management#
This comes up in almost every mobile system design interview I've conducted or participated in. The pattern is straightforward: intercept outgoing requests to add the auth token, and intercept 401 responses to refresh it.
Auth interceptor with token refresh#
On Android, OkHttp's interceptor API is built for exactly this. On iOS, you define a small interceptor protocol yourself — the same RequestInterceptor the client from earlier already runs.
class AuthInterceptor(
private val tokenProvider: TokenProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenProvider.getAccessToken()
val request = chain.request().newBuilder().apply {
token?.let { addHeader("Authorization", "Bearer $it") }
}.build()
val response = chain.proceed(request)
// If we get a 401 and have a refresh token, try to refresh
if (response.code == 401 && tokenProvider.getRefreshToken() != null) {
synchronized(this) {
// Check if another thread already refreshed the token
val currentToken = tokenProvider.getAccessToken()
if (currentToken != token) {
// Token was already refreshed by another request
response.close()
val retryRequest = request.newBuilder()
.header("Authorization", "Bearer $currentToken")
.build()
return chain.proceed(retryRequest)
}
// We need to actually refresh
val newToken = tokenProvider.refreshToken()
if (newToken != null) {
response.close()
val retryRequest = request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
return chain.proceed(retryRequest)
}
}
}
return response
}
}
protocol RequestInterceptor {
func intercept(_ request: URLRequest) async throws -> URLRequest
}
final class AuthInterceptor: RequestInterceptor {
private let tokenStore: TokenStore
private let tokenRefresher: TokenRefresher
private let lock = NSLock()
private var isRefreshing = false
init(tokenStore: TokenStore, tokenRefresher: TokenRefresher) {
self.tokenStore = tokenStore
self.tokenRefresher = tokenRefresher
}
func intercept(_ request: URLRequest) async throws -> URLRequest {
var mutableRequest = request
guard let token = tokenStore.accessToken else {
return mutableRequest
}
mutableRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return mutableRequest
}
}
The two implementations differ in scope. OkHttp interceptors see both the request and the response, so the Kotlin version handles the 401-refresh-retry cycle inline. The Swift interceptor only touches outgoing requests; on the response side, the API client checks for 401 and triggers a token refresh before retrying the original request. On both platforms, the tricky part is making sure only one refresh happens at a time — if five requests all get 401s simultaneously, you don't want five refresh calls.
Interview Tip: When you discuss token refresh in an interview, mention the race condition. Multiple concurrent requests can all receive 401s at the same time. If you don't synchronize the refresh, you'll fire multiple refresh requests and potentially invalidate tokens that are still valid. This is the kind of detail that separates senior candidates from everyone else.
Retry and timeout strategies#
Not every failed request should be retried. A 401 means the user isn't authenticated — retrying won't help. A 404 means the resource doesn't exist. But a timeout or a 503? Those are worth retrying.
Exponential backoff#
The standard approach: wait 1 second, then 2, then 4, then 8. Add jitter so thousands of clients don't all retry at the same instant.
On Android, OkHttp's retryOnConnectionFailure (on by default) transparently retries certain connection-level failures, but application-level retries with backoff — the kind that respect isRetryable — belong in a layer you own, just like on iOS.
data class RetryPolicy(
val maxAttempts: Int,
val baseDelayMillis: Long,
val maxDelayMillis: Long
) {
companion object {
val STANDARD = RetryPolicy(maxAttempts = 3, baseDelayMillis = 1_000, maxDelayMillis = 16_000)
}
fun delayFor(attempt: Int): Long {
val exponentialDelay = baseDelayMillis * 2.0.pow(attempt)
val clampedDelay = min(exponentialDelay, maxDelayMillis.toDouble())
val jitter = Random.nextDouble(0.0, clampedDelay * 0.25)
return (clampedDelay + jitter).toLong()
}
}
// Usage inside the network client
suspend fun <T> executeWithRetry(
retryPolicy: RetryPolicy = RetryPolicy.STANDARD,
block: suspend () -> T
): T {
var lastError: NetworkError? = null
repeat(retryPolicy.maxAttempts) { attempt ->
try {
return block()
} catch (error: NetworkError) {
if (!error.isRetryable) throw error // Non-retryable error — fail immediately
lastError = error
if (attempt < retryPolicy.maxAttempts - 1) {
delay(retryPolicy.delayFor(attempt))
}
}
}
throw lastError ?: NetworkError.Timeout
}
struct RetryPolicy {
let maxAttempts: Int
let baseDelay: TimeInterval
let maxDelay: TimeInterval
static let standard = RetryPolicy(maxAttempts: 3, baseDelay: 1.0, maxDelay: 16.0)
func delay(for attempt: Int) -> TimeInterval {
let exponentialDelay = baseDelay * pow(2.0, Double(attempt))
let clampedDelay = min(exponentialDelay, maxDelay)
let jitter = Double.random(in: 0...(clampedDelay * 0.25))
return clampedDelay + jitter
}
}
// Usage inside the network client
func executeWithRetry<T: Decodable>(
_ request: APIRequest,
retryPolicy: RetryPolicy = .standard
) async throws -> T {
var lastError: Error?
for attempt in 0..<retryPolicy.maxAttempts {
do {
return try await execute(request)
} catch let error as NetworkError where error.isRetryable {
lastError = error
if attempt < retryPolicy.maxAttempts - 1 {
let delay = retryPolicy.delay(for: attempt)
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
} catch {
throw error // Non-retryable error — fail immediately
}
}
throw lastError ?? NetworkError.invalidResponse
}
Timeouts#
Realistic timeout values for mobile:
| Operation | Timeout | Why |
|---|---|---|
| Connect | 10s | Establishes TCP connection. On cellular, handshakes are slow. |
| Read | 30s | Waiting for response data. Large payloads need breathing room. |
| Write | 30s | Uploading data. Media uploads can be large on slow connections. |
| Overall | 60s | The total ceiling. Users won't wait longer than this. |
On mobile, especially on cellular networks, you want slightly longer timeouts than you'd use on the web. A train going through a tunnel can cause a 5-second gap in connectivity — if your timeout is 5 seconds, you'll get false failures constantly.
Connectivity monitoring#
Your app should know whether the device is online before making a request. Not to block the request entirely — you might have cached data — but to adapt the UI and queue requests for later.
Observing connectivity changes#
On iOS, NWPathMonitor from the Network framework reports path changes. On Android, ConnectivityManager delivers network callbacks you can expose as a StateFlow.
class ConnectivityMonitor(context: Context) {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val _connectionState = MutableStateFlow(ConnectionState())
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
data class ConnectionState(
val isConnected: Boolean = true,
val type: ConnectionType = ConnectionType.UNKNOWN
)
enum class ConnectionType { WIFI, CELLULAR, UNKNOWN }
fun start() {
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
updateState(true)
}
override fun onLost(network: Network) {
updateState(false)
}
override fun onCapabilitiesChanged(
network: Network,
capabilities: NetworkCapabilities
) {
val type = when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ->
ConnectionType.WIFI
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
ConnectionType.CELLULAR
else -> ConnectionType.UNKNOWN
}
_connectionState.value = ConnectionState(isConnected = true, type = type)
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, networkCallback)
}
private fun updateState(isConnected: Boolean) {
_connectionState.value = _connectionState.value.copy(isConnected = isConnected)
}
}
import Network
final class ConnectivityMonitor: ObservableObject {
@Published var isConnected: Bool = true
@Published var connectionType: ConnectionType = .unknown
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "connectivity-monitor")
enum ConnectionType {
case wifi, cellular, wired, unknown
}
func start() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
self?.connectionType = self?.mapConnectionType(path) ?? .unknown
}
}
monitor.start(queue: queue)
}
func stop() {
monitor.cancel()
}
private func mapConnectionType(_ path: NWPath) -> ConnectionType {
if path.usesInterfaceType(.wifi) { return .wifi }
if path.usesInterfaceType(.cellular) { return .cellular }
if path.usesInterfaceType(.wiredEthernet) { return .wired }
return .unknown
}
}
Knowing the connection type also lets you make smart decisions. On Wi-Fi, prefetch aggressively. On cellular, be conservative. On metered connections, skip auto-playing videos. These are the kinds of trade-offs interviewers want to hear you reason about.
Testing the network layer#
If your network layer is hard to test, it's a sign the architecture is wrong. The protocol-based approach we set up earlier makes this straightforward.
Mocking the client, faking the server#
On iOS, the protocol-based client makes a hand-rolled mock trivial. On Android, MockWebServer from OkHttp's testing library lets you spin up a local HTTP server and enqueue exact responses.
@Test
fun `fetch user returns decoded user`() = runTest {
val mockServer = MockWebServer()
val responseBody = """
{
"id": "123",
"name": "Jane Doe",
"email": "jane@example.com",
"avatar_url": null,
"created_at": "2026-01-15T10:30:00Z",
"role": "member"
}
""".trimIndent()
mockServer.enqueue(MockResponse().setBody(responseBody).setResponseCode(200))
mockServer.start()
val apiService = NetworkModule.provideApiService(
baseUrl = mockServer.url("/").toString(),
tokenProvider = FakeTokenProvider()
)
val user = apiService.getUser("123")
assertEquals("123", user.id)
assertEquals("Jane Doe", user.name)
assertEquals(UserRole.MEMBER, user.role)
mockServer.shutdown()
}
final class MockNetworkClient: NetworkClient {
var stubbedResult: Any?
var stubbedError: Error?
var executedRequests: [APIRequest] = []
func execute<T: Decodable>(_ request: APIRequest) async throws -> T {
executedRequests.append(request)
if let error = stubbedError {
throw error
}
guard let result = stubbedResult as? T else {
fatalError("Stubbed result type mismatch. Expected \(T.self)")
}
return result
}
}
// In your test
func testFetchUserReturnsDecodedUser() async throws {
let mockClient = MockNetworkClient()
let expectedUser = UserResponse(
id: "123",
name: "Jane Doe",
email: "jane@example.com",
avatarUrl: nil,
createdAt: Date(),
role: .member
)
mockClient.stubbedResult = expectedUser
let repository = UserRepository(networkClient: mockClient)
let user = try await repository.fetchUser(id: "123")
XCTAssertEqual(user.id, "123")
XCTAssertEqual(user.name, "Jane Doe")
XCTAssertEqual(mockClient.executedRequests.count, 1)
XCTAssertEqual(mockClient.executedRequests.first?.path, "users/123")
}
Both approaches let you test without hitting real servers, without mocking HTTP internals, and without flaky network dependencies in your CI pipeline.
How to present this in an interview#
In a system design interview, you don't have time to code a full network layer. That's not the point. The point is to show that you've thought about it and can make informed trade-offs.
Here's how I structure the networking section when I'm whiteboarding a design:
Step 1: State the architecture in one sentence. "I'll use a centralized API client with interceptors for auth and retry, and a typed error hierarchy so the UI always knows what happened."
Step 2: Draw the layers. Show the API client sitting between the repository/use-case layer and the HTTP transport. Draw the interceptor chain.
┌──────────────────────────────┐
│ Repository / Use Case │
├──────────────────────────────┤
│ API Client │
├──────────────────────────────┤
│ Interceptor Chain │
│ (Auth → Logging → Retry) │
├──────────────────────────────┤
│ URLSession / OkHttp │
└──────────────────────────────┘
Step 3: Call out the specific decisions. "I'm using exponential backoff with jitter for retries. I retry on 5xx and timeouts, never on 4xx. Token refresh is synchronized so concurrent 401s don't trigger multiple refreshes."
Step 4: Mention testability. "The API client sits behind a protocol, so I inject a mock in unit tests. Integration tests use a local mock server."
Interviewers don't expect you to write out every line. They expect you to know what the network layer does, why each piece exists, and where the failure points are. If you can articulate the token refresh race condition, the retry backoff calculation, and the error type hierarchy in two minutes on a whiteboard, you've demonstrated more production experience than most candidates.
Interview Tip: When time is limited, focus on the parts of the network layer that are specific to the problem you're designing. Building a chat app? Talk about WebSocket reconnection and message delivery guarantees. Building a feed? Talk about pagination, caching headers, and prefetching. The network layer isn't a generic box — it should reflect the product requirements.
The network layer isn't glamorous. Nobody gets promoted for writing a great AuthInterceptor. But it's the foundation everything else sits on, and in an interview, a clear explanation of how you'd build it signals that you've done this work for real.