On This Page
1Overview2Critical Anti-Patterns
3High-Severity Anti-Patterns4Medium-Severity Anti-Patterns
5Anti-Patterns to Avoid6References

Overview

Anti-patterns are categorised by severity: Critical (produce security vulnerabilities, data loss, or production crashes), High (produce significant technical debt and degraded quality), and Medium (reduce maintainability and team velocity without immediate production impact). Critical anti-patterns are blocking code review findings — no PR that introduces a critical anti-pattern is merged.

Critical Anti-Patterns

Credentials in Plaintext Storage

Symptom: Session tokens, API keys, or passwords stored in SharedPreferences, UserDefaults, or a plaintext SQLite column.
Root Cause: Developer unaware of platform secure storage APIs. Speed priority over security.
Consequence: Any party with filesystem access (rooted/jailbroken device, backup extraction, physical access) reads the credential.
Correct approach: Android Keystore + EncryptedSharedPreferences. iOS Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly.

⚠ Plaintext Credential StoragesharedPrefs.putString("token", accessToken) on Android or UserDefaults.standard.set(accessToken, forKey: "token") on iOS. P1 security finding. Blocking code review gate.
CORRECT: EncryptedSharedPreferences.create(...) on Android. KeychainSwift().set(accessToken, forKey: "token") with appropriate accessibility attribute on iOS.

Network Call in ViewModel

Symptom: ViewModel contains Retrofit or URLSession import. apiService.getAccount() called directly from ViewModel.
Root Cause: Architect specified clean architecture verbally but did not enforce it through static analysis or code review.
Consequence: Business logic mixed with network I/O. ViewModel untestable without mocking the entire network stack. Security audit cannot identify the data layer boundary.
Correct approach: Network calls are in Repository implementations only. ViewModel calls Use Cases. Use Cases call Repository interfaces.

⚠ Network Call in ViewModelviewModelScope.launch { val account = apiService.getAccount(id) ... } in a ViewModel class.
CORRECT: viewModelScope.launch { val result = getAccountUseCase(id); _state.update { it.copy(account = result.getOrNull()) } }. The Use Case handles the Repository call.

Implicit Flow for OAuth

Symptom: OAuth authorization URL contains response_type=token. Access token appears in the redirect URL fragment.
Root Cause: OAuth Implicit Flow was the recommended mobile pattern before 2017. Many tutorials still reference it.
Consequence: Access token transmitted in URL fragment is visible in server logs, browser history, and referrer headers. Attack surface for token interception.
Correct approach: OAuth 2.0 Authorization Code + PKCE. response_type=code. Access token received only in the token endpoint response body.

High-Severity Anti-Patterns

⚠ God ViewModel — ViewModel with 600+ lines handling networking, navigation, business logic, analytics, error handling, and UI state. Impossible to test, constant merge conflicts for large teams.
CORRECT: Maximum 200 lines. Business logic in Use Cases. Navigation events in a dedicated NavigationEvent sealed class. Analytics in a separate event observer. Each concern in its own class.

⚠ Mutable State Exposed from ViewModelval accounts: MutableList<Account> as a public property. Any class can add, remove, or reorder accounts.
CORRECT: Expose only immutable state. val uiState: StateFlow<AccountsUiState>. Internal mutable state is private val _uiState: MutableStateFlow<AccountsUiState>.

⚠ Hardcoded Strings for Configuration — API base URL, feature flags, or environment identifiers hardcoded as string literals in source code. Different teams maintain different source files for different environments.
CORRECT: Android BuildConfig fields injected from Gradle build variants. iOS xcconfig files with environment-specific values. CI injects values from encrypted secrets at build time.

⚠ Synchronous Network on Background Thread in Loop — Fetching 500 records by making 500 sequential API calls in a for loop on a background thread. Each call adds 200ms on 4G — 100 seconds total.
CORRECT: Batch endpoint returns all 500 records in a single API call. If a batch endpoint is not available, concurrent requests using async/await with withContext(Dispatchers.IO).

Medium-Severity Anti-Patterns

⚠ Context Passed as Use Case ParameterGetUserUseCase(context: Context) — the domain layer has a dependency on the Android platform.
CORRECT: Domain layer imports no Android classes. Values that require Context (resources, file paths) are resolved at the Repository or data layer and passed as primitive types to the domain.

⚠ Magic Numbers in Domain Logicif (account.balance > 50000) { applyPremiumTier() } — business threshold embedded as a literal in source code.
CORRECT: Named constants in the domain model: const val PREMIUM_TIER_THRESHOLD = 50_000. Document the business rule in a comment. Ideally, fetch the threshold from configuration to enable adjustment without a release.

Anti-Patterns to Avoid

⚠ 1. Credentials in Plaintext Storage

Session tokens or API keys written to SharedPreferences or UserDefaults, readable on any rooted or jailbroken device. A P1 finding that exposes every user's session.

Hover to see the fix ↻
↺ Correct Approach

Android Keystore + EncryptedSharedPreferences; iOS Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Hardware-backed, device-bound, never embedded in the binary.

⚠ 2. Network Call in the ViewModel

apiService.get() invoked directly from a ViewModel, collapsing the data layer into presentation. Untestable without mocking the network stack and a standing security-boundary violation.

Hover to see the fix ↻
↺ Correct Approach

ViewModel → Use Case → Repository interface → data source. Each layer is independently testable and the network boundary stays in the data layer.

Flowchart

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD subgraph CRITICAL["🔴 Critical — Blocking Code Review Gate"] AP1["<b>Credentials in Plaintext Storage</b><br/>SharedPreferences / UserDefaults<br/>Readable on rooted devices<br/><i>P1 Security Finding</i>"] AP2["<b>Network Call in ViewModel</b><br/>apiService.get() from ViewModel<br/>Security boundary violated<br/><i>Architecture Violation</i>"] AP3["<b>OAuth Implicit Flow</b><br/>client_secret in binary<br/>Extractable in &lt; 60 seconds<br/><i>Token Compromise Risk</i>"] end subgraph HIGH["🟠 High — Mandatory Refactor"] AP4["<b>God ViewModel</b><br/>&gt; 200 lines<br/>Networking + business logic<br/>+ navigation + analytics"] AP5["<b>Mutable State Exposed</b><br/>public var list = mutableListOf()<br/>No single source of truth"] AP6["<b>Hardcoded Configuration</b><br/>API URLs · Feature flags in code<br/>Environment management failure"] end subgraph CORRECT["✅ Correct Approaches"] C1["<b>Keystore + EncryptedSharedPrefs</b><br/>iOS Keychain<br/>Hardware-backed storage"] C2["<b>Repository → UseCase → ViewModel</b><br/>Layer boundary enforced<br/>Each layer independently testable"] C3["<b>OAuth 2.0 + PKCE</b><br/>AppAuth library<br/>No client_secret required"] end AP1 -->|"Fix"| C1 AP2 -->|"Fix"| C2 AP3 -->|"Fix"| C3 style CRITICAL fill:#FEE2E2,stroke:#DC2626,stroke-width:2px style HIGH fill:#FEF3C7,stroke:#D97706,stroke-width:2px style CORRECT fill:#DCFCE7,stroke:#16A34A,stroke-width:2px style AP1 fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d style AP2 fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d style AP3 fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d style AP4 fill:#FDE68A,stroke:#D97706,color:#78350F style AP5 fill:#FDE68A,stroke:#D97706,color:#78350F style AP6 fill:#FDE68A,stroke:#D97706,color:#78350F style C1 fill:#BBF7D0,stroke:#16A34A,color:#14532D style C2 fill:#BBF7D0,stroke:#16A34A,color:#14532D style C3 fill:#BBF7D0,stroke:#16A34A,color:#14532D

References

  1. Fowler, Martin — Refactoring: Improving the Design of Existing Code. Addison-Wesley, 2018.
  2. Google — Android Common Anti-patterns. developer.android.com/topic/performance/vitals
  3. OWASP — Mobile Application Security Verification Standard. owasp.org/www-project-mobile-app-security
  4. Nygard, Michael — Release It! Pragmatic Bookshelf, 2018.
Mobile Engineering Reference
← Mobile Development