Coupling in Mobile Development — Key Concepts, Types and How to Reduce It

Author: IT Sectr Published: 2026-05-13 Reading time: 9 min

Coupling is a metric that shows how much one module of an application depends on another. According to Wikipedia, loose coupling (low coupling) is a sign of a well-designed system where modules can be changed without breaking neighboring ones. Managing coupling is one of the main tasks of an architect when designing mobile applications.

Key Takeaways

  • Coupling — the degree of dependency between modules: high = tight coupling, low = loose coupling
  • Content coupling — the worst type, when a module modifies internal data of another module
  • Data coupling — the best type, when modules exchange only simple data through parameters
  • Dependency Injection — the main tool for reducing coupling in mobile development
  • Interfaces and abstractions — the main mechanism for reducing coupling between application layers

What is Coupling

Coupling is a metric that determines how tightly one module or class is connected to another. The more one module knows about the internal structure of another, the higher the coupling and the harder it is to change the system. In a well-designed architecture, coupling should be minimal — modules interact only through strictly defined interfaces.

There are two sides of coupling: afferent (incoming dependencies — how many modules depend on this one) and efferent (outgoing dependencies — how many modules this one depends on). Analyzing these metrics helps identify hot spots in the architecture where changing one module would affect many others. Tools like IntelliJ Dependency Analyzer and Xcode Graph visualize these connections.

It is important to understand that zero coupling is impossible — modules need to interact somehow, otherwise it is not a system but a set of isolated programs. The architect's task is to make coupling manageable and transparent. The ideal: modules interact only through interfaces and pass only simple data, without knowing about each other's internal structure. This is called loose coupling.

Types of Coupling from Weak to Strong

Six types of coupling form a scale from best to worst. Understanding this scale helps evaluate existing code and choose the direction of refactoring. Most mobile projects have mixed coupling types, and the architect's task is to progressively replace strong types with weak ones.

Data coupling — the best type

Data coupling — modules exchange only simple data through method parameters. Module A calls module B's method, passing primitives or simple structures, and receives a result. Module A does not know how B is implemented internally. This is the most desirable type of coupling: it minimizes the impact of changes.

Example: EmailValidator.isValid(email: String): Boolean. The consumer class passes a string and receives a Boolean, having no idea about the regular expressions or validation rules inside the validator. Changing the validation logic does not require changing the consumer — coupling is minimal. Data coupling is the goal for all public interfaces in an application.

Stamp coupling — acceptable but not ideal

Stamp coupling — modules exchange composite objects but use only part of their fields. Module A passes a User object to the calculateDiscount method, which uses only user.status. The problem: if the User structure changes (a required field is added), the calculateDiscount module does not change, but the consumer creating the User object does.

In practice, stamp coupling is inevitable and acceptable if the passed object is a standard data model (Entity). The problem arises when a module receives an entire object just for a single field. In such cases, it is better to pass the specific value directly (data coupling). The solution is to analyze the field usage by the receiving side.

Control, External, Common and Content coupling

Control coupling — one module passes a flag to another that controls its behavior (calculate(useNewAlgorithm: Boolean)). This is worse than stamp coupling because the consumer module must know the internal operating variants of the called module. Solution: split the method into two — calculateWithNewAlgorithm() and calculateWithLegacyAlgorithm().

External coupling — modules depend on an external protocol, data format, or API. All modules that parse the same JSON or work with the same database have external coupling. It cannot be completely avoided, but it can be isolated: create a mapping layer between the external format and internal models. Common coupling — modules share a common global state. Content coupling — the worst type, when a module directly modifies the internal data of another module.

Coupling TypeLevelDescription
DataBestPassing simple data through parameters
StampAcceptablePassing objects with partial usage
ControlMediumControlling behavior through flags
ExternalHighDependency on external protocol/format
CommonVery HighSharing global state
ContentUnacceptableDirect modification of module internal data

The coupling scale from data (ideal) to content (disaster) is a practical tool for code reviews. If you see common or content coupling in a project — these are priority refactoring targets. Data and stamp coupling are acceptable and present in any project, but their quantity should be controlled.

Why Coupling is Critical in Mobile Development

High coupling turns development into a slow process where every change requires checking dozens of potentially broken modules. This is especially critical in mobile development: platforms update annually (Android API Level, iOS SDK), libraries quarterly, and business requirements continuously. Loose coupling is the only way to handle this flow of changes without constant regressions.

Practical example: a mobile app where all screens directly import NetworkingManager and DatabaseManager. When replacing the HTTP client from Retrofit to Ktor (Android) or from URLSession to Alamofire (iOS), the developer would have to modify every screen. With low coupling, it is enough to change one implementation hidden behind the NetworkDataSource interface — consumers will not notice the replacement.

The impact of coupling on unit testing is also huge. A class with high coupling (direct creation of dependencies through constructor) cannot be tested in isolation — it drags along the database, network, and UI. To test such a class, you have to launch an emulator and wait for integration tests. A class with low coupling accepts dependencies through constructor injection and is easily mockable.

kotlin
// High coupling — class creates its own dependencies
class ProfileViewModelHigh {
    private val api = RetrofitApi()
    private val db = RoomDatabase.getInstance()
    private val cache = MemoryCache()
}

// Low coupling — dependencies are passed through constructor
class ProfileViewModelLow(
    private val api: ApiService,
    private val db: DatabaseService,
    private val cache: CacheService
)

In the first case, ProfileViewModelHigh is tightly bound to specific implementations — replacing Retrofit with Ktor requires changing the ViewModel code. In the second case, ProfileViewModelLow depends only on interfaces, whose implementations are provided externally. Testing the second class is trivial: pass mock implementations and verify logic without an emulator.

Patterns to Reduce Coupling

Dependency Inversion Principle (D in SOLID) is the foundation for reducing coupling. The principle dictates depending on abstractions, not concrete implementations. Instead of a class directly creating a RetrofitApi object, it should receive an ApiService interface. This shifts the dependency from a specific library to the abstraction level, which can be replaced without changing the consumer.

Observer pattern (or its reactive versions — StateFlow, Combine Publishers) reduces coupling between data source and subscribers. The subscriber does not know where the data comes from — it simply reacts to changes. This decouples the sender and receiver: you can add a new data source without changing existing subscribers. EventBus and SharedFlow work on the same principle.

Bridge pattern separates abstraction from implementation, allowing them to change independently. In mobile development, Bridge is used, for example, for platform-dependent modules: a common ImageLoader interface with different implementations for iOS (Kingfisher, Nuke) and Android (Glide, Coil). Code working with ImageLoader does not depend on the chosen library and can replace it by simply changing the implementation.

Dependency Injection as a Coupling Management Tool

Dependency Injection (DI) is the most practical tool for reducing coupling in mobile development. Instead of a class creating its own dependencies, a DI container (Hilt, Koin, Dagger for Android; Swinject, Factory for iOS) provides them from outside. The class receives dependencies through constructor, method, or property injection, remaining unaware of the concrete implementations.

DI explicitly documents class dependencies: just look at the constructor to understand which modules the class interacts with. If the constructor accepts 8 parameters from different layers — this is a signal of excessive coupling requiring refactoring. Good practice is no more than 3-4 dependencies per class. More indicates a violation of Single Responsibility and excessive coupling.

DI also simplifies testing: for each test you create a class with mock dependencies, without requiring a real database or network. In Flutter DI is implemented through Provider, Riverpod, or GetIt. Regardless of the framework, the goal is one: reduce coupling between modules by making dependencies explicit and replaceable. Using DI in a mobile project has been the de facto standard since the 2020s.

swift
// DI container builds the dependency graph
protocol AuthServiceProtocol {
    func login(email: String, password: String) async throws -> User
}

final class AuthService: AuthServiceProtocol {
    func login(email: String, password: String) async throws -> User {
        // implementation
    }
}

// ViewModel does not know about the specific service — only the protocol
final class LoginViewModel {
    private let auth: AuthServiceProtocol

    init(auth: AuthServiceProtocol) {
        self.auth = auth
    }
}

// DI Container is the only place where concrete types are created
final class DIContainer {
    lazy var authService: AuthServiceProtocol = AuthService()
    lazy var loginViewModel: LoginViewModel {
        LoginViewModel(auth: self.authService)
    }
}

Here, LoginViewModel depends only on the AuthServiceProtocol, not on a specific AuthService. Replacing the implementation (e.g., switching from Firebase Auth to a custom server) requires changes only in DIContainer. All consumers of AuthServiceProtocol remain untouched — coupling is minimized through abstraction and DI.

Frequently Asked Questions

How does coupling differ from cohesion?

Cohesion measures the internal consistency of a module, while coupling measures the external interconnectedness between modules. Good architecture strives for high cohesion and low coupling. These metrics are inversely proportional: increasing cohesion usually reduces coupling, and vice versa.

What type of coupling is acceptable in production code?

Data and stamp are normal and present in any project. Control coupling is acceptable in limited scenarios (e.g., strategy pattern). External coupling is inevitable when working with external APIs but should be isolated behind a mapping layer. Common and content coupling are signs of architectural problems requiring immediate refactoring.

How to measure coupling in a project?

Static analysis tools: IntelliJ IDEA Dependency Matrix, Xcode Graph, Gradle Dependencies report, SonarQube. Metrics: afferent coupling (Ca), efferent coupling (Ce), Instability (Ce/(Ca+Ce)). High Instability (close to 1) means the module is easy to change and few things reference it — this is good.

Can low coupling be harmful?

Extremely low coupling can mean an excessive number of abstractions and interfaces that complicate code navigation. If a separate interface is created for every class, the programmer spends time jumping between files. Balance: interfaces for the module's external API, but not for every internal helper class.

How to reduce coupling when working with legacy code?

Use the Strangler Fig technique — gradually replace direct calls with interfaces. Start by extracting interfaces for the most frequently referenced classes. Then introduce a DI container. Cover the isolated code with characterization tests to ensure the refactoring does not change system behavior.

Summary

  • Coupling — a metric of dependency between modules: loose coupling is the goal of good architecture
  • Data coupling — the best type, content coupling — the worst, unacceptable in production code
  • Dependency Inversion and interfaces — the main mechanisms for reducing coupling
  • Dependency Injection — a practical tool that makes dependencies explicit and replaceable
  • High coupling makes code fragile: one change breaks many modules
  • Low coupling simplifies testing: each module is mocked independently without an emulator
  • Balance between coupling and abstractions — excessive interfaces complicate the code

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also