Mobile SDK Architect (Fintech Kotlin)
Workflow en étapes
1. Analyse des besoins et périmètre
- Identifier les cas d'usage : KYC, onboarding, paiements P2P, notifications push, réconciliation offline.
- Définir la frontière SDK : ce qui est public (façade) vs interne (implémentation).
- Choisir la cible : Android natif, KMP (Kotlin Multiplatform), ou React Native bridge.
Critère de décision — KMP vs natif vs bridge :
| Contexte |
Choix |
| Équipe Kotlin existante, logique métier à partager |
KMP (shared commonMain) |
| App mobile unique, Android only |
Module Gradle natif |
| App Flutter/RN existante |
Bridge natif (FFI ou JNI/Obj-C) |
| Time-to-market prioritaire |
React Native SDK |
2. Structure modulaire Gradle / KMP
sdk/
├── core/ # entités, use cases, interfaces (pas d'Android)
├── network/ # Retrofit/Ktor, Serialization
├── storage/ # Room / SQLDelight (KMP)
├── auth/ # OAuth2, JWT, token refresh
├── payments/ # logique paiement, state machine
└── sample-app/ # démo intégration
Déclaration multiplatform (build.gradle.kts) :
kotlin {
androidTarget()
iosX64(); iosArm64(); iosSimulatorArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:2.3.12")
implementation("app.cash.sqldelight:runtime:2.0.2")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.12")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.12")
}
}
}
}
3. API publique — design et rétrocompatibilité
- Exposer uniquement via une façade (
internal pour tout le reste).
- Appliquer semver strict : MAJOR = breaking, MINOR = ajout, PATCH = fix.
- Marquer les deprecations avec
@Deprecated(level = DeprecationLevel.WARNING) avant retrait.
// facade publique
class FintechSDK private constructor(private val config: SDKConfig) {
companion object {
fun init(config: SDKConfig): FintechSDK = FintechSDK(config)
}
val payments: PaymentsApi get() = PaymentsApiImpl(...)
val kyc: KycApi get() = KycApiImpl(...)
}
// jamais exposer :
internal class PaymentsApiImpl(...) : PaymentsApi { ... }
- Versionner les endpoints :
/api/v2/payments, jamais supprimer v1 sans migration guide.
- Feature flags via
SDKConfig pour activer/désactiver modules sans rebuild.
4. Sécurité et conformité (fintech 2026)
- TLS 1.3 obligatoire + certificate pinning (OkHttp
CertificatePinner).
- Android Keystore pour clés AES-256-GCM ; iOS Keychain côté Swift.
- Token refresh : rotation JWT avec sliding window, révocation côté serveur.
- RGPD / CNDP Maroc : anonymisation PII dans les logs, droit à l'effacement via API dédiée.
// Certificate pinning OkHttp
val client = OkHttpClient.Builder()
.certificatePinner(
CertificatePinner.Builder()
.add("api.myfintech.com", "sha256/AAAA...==")
.build()
)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
- Ne jamais logger de données sensibles (PAN, IBAN, token) — utiliser un
SensitiveDataFilter sur le logger.
- Root/jailbreak detection (SafetyNet/Play Integrity API 2026 ;
DCAppAttestService iOS).
5. Error handling standardisé
Sealed class pour erreurs métier, jamais de try/catch silencieux :
sealed class SdkResult<out T> {
data class Success<T>(val data: T) : SdkResult<T>()
sealed class Failure : SdkResult<Nothing>() {
data class Network(val code: Int, val msg: String) : Failure()
data class Business(val errorCode: String, val msg: String) : Failure()
data class Unexpected(val cause: Throwable) : Failure()
}
}
// codes métier exhaustifs
object ErrorCodes {
const val KYC_FAILED = "ERR_KYC_001"
const val AUTH_EXPIRED = "ERR_AUTH_002"
const val INSUFFICIENT_BALANCE = "ERR_PAY_003"
}
6. Performance mobile
- Offline-first : Room / SQLDelight comme source de vérité locale ; sync via WorkManager.
- Pagination cursor-based (
after=<id>) — jamais offset sur grandes collections.
- Cache HTTP : OkHttp Cache 10 MB pour endpoints stables ;
Cache-Control: max-age côté serveur.
- Lazy init des modules lourds (ex. : caméra KYC) — ne pas tout charger au
SDK.init().
// WorkManager sync périodique
val syncRequest = PeriodicWorkRequestBuilder<TransactionSyncWorker>(
repeatInterval = 15, TimeUnit.MINUTES
).setConstraints(
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"tx_sync", ExistingPeriodicWorkPolicy.KEEP, syncRequest
)
7. Testing
| Couche |
Outil |
Cible |
| Domain (use cases, entities) |
JUnit5 + MockK |
> 90 % |
| Data (Retrofit, Room) |
Hilt test + Robolectric |
> 75 % |
| UI / Compose |
Compose Testing + Espresso |
smoke tests |
| Contrat API |
WireMock / MockWebServer |
régression |
| Performance |
Android Profiler + Benchmark |
baseline |
// MockK use case test
@Test
fun `transfer fails on insufficient balance`() = runTest {
coEvery { repository.getBalance(any()) } returns 10.0
val result = transferUseCase(TransferRequest(amount = 500.0, ...))
assertIs<SdkResult.Failure.Business>(result)
assertEquals(ErrorCodes.INSUFFICIENT_BALANCE, result.errorCode)
}
8. Documentation et publication
- KDoc sur tout symbole public ; générer la référence via
./gradlew dokkaHtml.
- README avec quickstart en < 10 lignes (copier-coller prêt).
- Sample app dans
sample-app/ couvrant les 3 flux principaux.
- Publier sur Maven Central ou dépôt privé (Nexus/GitHub Packages) :
// publishing block (build.gradle.kts)
publishing {
publications {
create<MavenPublication>("release") {
groupId = "com.mycompany"
artifactId = "fintech-sdk"
version = "2.1.0"
from(components["release"])
}
}
}
Garde-fous / Anti-patterns
| Anti-pattern |
Conséquence |
Correction |
| Exposer des classes internes dans l'API publique |
Breaking changes incontrôlés |
internal + façade |
| Crasher au lieu de retourner une erreur métier |
App cliente plante |
SdkResult.Failure systématique |
| Bloquer le main thread (réseau, DB) |
ANR |
Coroutines + Dispatchers.IO |
| Stocker tokens en SharedPreferences non chiffrées |
Vol de session |
EncryptedSharedPreferences / Keystore |
| Pagination offset sur > 10 000 lignes |
Timeouts, duplicats |
Cursor-based (after=<id>) |
| Logger PAN/IBAN en clair |
Non-conformité PCI-DSS |
SensitiveDataFilter obligatoire |
| Init SDK bloquant au démarrage |
Temps de lancement > 500 ms |
Lazy init + coroutine scope |
| Semver ignoré (breaking en MINOR) |
Intégrations cassées en prod |
Revue API diff avant release |
Bonnes pratiques 2026
- Play Integrity API remplace SafetyNet depuis 2024 — migrer si pas fait.
- Predictive Back Gesture Android 15+ : tester les transitions SDK dans le back stack.
- KMP stable (Kotlin 2.x) : privilégier
commonMain pour la logique métier, éviter les expect/actual inutiles.
- Kotlin Coroutines Structured Concurrency : toujours lier les coroutines à un
CoroutineScope géré par le cycle de vie ; jamais GlobalScope.
- Ktor 3.x : multiplatform natif, remplace OkHttp côté KMP shared layer.
- SQLDelight 2.x : préférer à Room pour KMP, schéma vérifié à la compilation.
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.
1---2name: dev-mobile-sdk-architect3description: Architecture d'un SDK mobile fintech en Kotlin Multiplatform — découpage en modules, API publique, couches, design patterns, sécurité et taille du binaire. Se déclenche avec "architecture SDK", "SDK mobile", "multiplatform", "KMP", "SDK fintech", "API publique du SDK", "module KMP". Also triggers on "mobile SDK design", "Kotlin Multiplatform SDK", "public API for an SDK".4---56# Mobile SDK Architect (Fintech Kotlin)78## Workflow en étapes910### 1. Analyse des besoins et périmètre1112- Identifier les cas d'usage : KYC, onboarding, paiements P2P, notifications push, réconciliation offline.13- Définir la frontière SDK : ce qui est public (façade) vs interne (implémentation).14- Choisir la cible : Android natif, KMP (Kotlin Multiplatform), ou React Native bridge.1516**Critère de décision — KMP vs natif vs bridge :**1718| Contexte | Choix |19|---|---|20| Équipe Kotlin existante, logique métier à partager | KMP (shared `commonMain`) |21| App mobile unique, Android only | Module Gradle natif |22| App Flutter/RN existante | Bridge natif (FFI ou JNI/Obj-C) |23| Time-to-market prioritaire | React Native SDK |2425---2627### 2. Structure modulaire Gradle / KMP2829```30sdk/31├── core/ # entités, use cases, interfaces (pas d'Android)32├── network/ # Retrofit/Ktor, Serialization33├── storage/ # Room / SQLDelight (KMP)34├── auth/ # OAuth2, JWT, token refresh35├── payments/ # logique paiement, state machine36└── sample-app/ # démo intégration37```3839Déclaration multiplatform (`build.gradle.kts`) :4041```kotlin42kotlin {43 androidTarget()44 iosX64(); iosArm64(); iosSimulatorArm64()4546 sourceSets {47 val commonMain by getting {48 dependencies {49 implementation("io.ktor:ktor-client-core:2.3.12")50 implementation("app.cash.sqldelight:runtime:2.0.2")51 }52 }53 val androidMain by getting {54 dependencies {55 implementation("io.ktor:ktor-client-okhttp:2.3.12")56 }57 }58 val iosMain by getting {59 dependencies {60 implementation("io.ktor:ktor-client-darwin:2.3.12")61 }62 }63 }64}65```6667---6869### 3. API publique — design et rétrocompatibilité7071- Exposer uniquement via une **façade** (`internal` pour tout le reste).72- Appliquer **semver strict** : MAJOR = breaking, MINOR = ajout, PATCH = fix.73- Marquer les deprecations avec `@Deprecated(level = DeprecationLevel.WARNING)` avant retrait.7475```kotlin76// facade publique77class FintechSDK private constructor(private val config: SDKConfig) {78 companion object {79 fun init(config: SDKConfig): FintechSDK = FintechSDK(config)80 }81 val payments: PaymentsApi get() = PaymentsApiImpl(...)82 val kyc: KycApi get() = KycApiImpl(...)83}8485// jamais exposer :86internal class PaymentsApiImpl(...) : PaymentsApi { ... }87```8889- Versionner les endpoints : `/api/v2/payments`, jamais supprimer v1 sans migration guide.90- Feature flags via `SDKConfig` pour activer/désactiver modules sans rebuild.9192---9394### 4. Sécurité et conformité (fintech 2026)9596- **TLS 1.3 obligatoire** + certificate pinning (OkHttp `CertificatePinner`).97- **Android Keystore** pour clés AES-256-GCM ; **iOS Keychain** côté Swift.98- **Token refresh** : rotation JWT avec sliding window, révocation côté serveur.99- **RGPD / CNDP Maroc** : anonymisation PII dans les logs, droit à l'effacement via API dédiée.100101```kotlin102// Certificate pinning OkHttp103val client = OkHttpClient.Builder()104 .certificatePinner(105 CertificatePinner.Builder()106 .add("api.myfintech.com", "sha256/AAAA...==")107 .build()108 )109 .connectTimeout(10, TimeUnit.SECONDS)110 .readTimeout(30, TimeUnit.SECONDS)111 .build()112```113114- Ne jamais logger de données sensibles (PAN, IBAN, token) — utiliser un `SensitiveDataFilter` sur le logger.115- Root/jailbreak detection (SafetyNet/Play Integrity API 2026 ; `DCAppAttestService` iOS).116117---118119### 5. Error handling standardisé120121Sealed class pour erreurs métier, jamais de `try/catch` silencieux :122123```kotlin124sealed class SdkResult<out T> {125 data class Success<T>(val data: T) : SdkResult<T>()126 sealed class Failure : SdkResult<Nothing>() {127 data class Network(val code: Int, val msg: String) : Failure()128 data class Business(val errorCode: String, val msg: String) : Failure()129 data class Unexpected(val cause: Throwable) : Failure()130 }131}132133// codes métier exhaustifs134object ErrorCodes {135 const val KYC_FAILED = "ERR_KYC_001"136 const val AUTH_EXPIRED = "ERR_AUTH_002"137 const val INSUFFICIENT_BALANCE = "ERR_PAY_003"138}139```140141---142143### 6. Performance mobile144145- **Offline-first** : Room / SQLDelight comme source de vérité locale ; sync via WorkManager.146- **Pagination cursor-based** (`after=<id>`) — jamais offset sur grandes collections.147- **Cache HTTP** : OkHttp Cache 10 MB pour endpoints stables ; `Cache-Control: max-age` côté serveur.148- **Lazy init** des modules lourds (ex. : caméra KYC) — ne pas tout charger au `SDK.init()`.149150```kotlin151// WorkManager sync périodique152val syncRequest = PeriodicWorkRequestBuilder<TransactionSyncWorker>(153 repeatInterval = 15, TimeUnit.MINUTES154).setConstraints(155 Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()156).build()157WorkManager.getInstance(context).enqueueUniquePeriodicWork(158 "tx_sync", ExistingPeriodicWorkPolicy.KEEP, syncRequest159)160```161162---163164### 7. Testing165166| Couche | Outil | Cible |167|---|---|---|168| Domain (use cases, entities) | JUnit5 + MockK | > 90 % |169| Data (Retrofit, Room) | Hilt test + Robolectric | > 75 % |170| UI / Compose | Compose Testing + Espresso | smoke tests |171| Contrat API | WireMock / MockWebServer | régression |172| Performance | Android Profiler + Benchmark | baseline |173174```kotlin175// MockK use case test176@Test177fun `transfer fails on insufficient balance`() = runTest {178 coEvery { repository.getBalance(any()) } returns 10.0179 val result = transferUseCase(TransferRequest(amount = 500.0, ...))180 assertIs<SdkResult.Failure.Business>(result)181 assertEquals(ErrorCodes.INSUFFICIENT_BALANCE, result.errorCode)182}183```184185---186187### 8. Documentation et publication188189- **KDoc** sur tout symbole public ; générer la référence via `./gradlew dokkaHtml`.190- README avec quickstart en < 10 lignes (copier-coller prêt).191- Sample app dans `sample-app/` couvrant les 3 flux principaux.192- Publier sur Maven Central ou dépôt privé (Nexus/GitHub Packages) :193194```kotlin195// publishing block (build.gradle.kts)196publishing {197 publications {198 create<MavenPublication>("release") {199 groupId = "com.mycompany"200 artifactId = "fintech-sdk"201 version = "2.1.0"202 from(components["release"])203 }204 }205}206```207208---209210## Garde-fous / Anti-patterns211212| Anti-pattern | Conséquence | Correction |213|---|---|---|214| Exposer des classes internes dans l'API publique | Breaking changes incontrôlés | `internal` + façade |215| Crasher au lieu de retourner une erreur métier | App cliente plante | `SdkResult.Failure` systématique |216| Bloquer le main thread (réseau, DB) | ANR | Coroutines + `Dispatchers.IO` |217| Stocker tokens en SharedPreferences non chiffrées | Vol de session | `EncryptedSharedPreferences` / Keystore |218| Pagination offset sur > 10 000 lignes | Timeouts, duplicats | Cursor-based (`after=<id>`) |219| Logger PAN/IBAN en clair | Non-conformité PCI-DSS | `SensitiveDataFilter` obligatoire |220| Init SDK bloquant au démarrage | Temps de lancement > 500 ms | Lazy init + coroutine scope |221| Semver ignoré (breaking en MINOR) | Intégrations cassées en prod | Revue API diff avant release |222223---224225## Bonnes pratiques 2026226227- **Play Integrity API** remplace SafetyNet depuis 2024 — migrer si pas fait.228- **Predictive Back Gesture** Android 15+ : tester les transitions SDK dans le back stack.229- **KMP stable** (Kotlin 2.x) : privilégier `commonMain` pour la logique métier, éviter les `expect/actual` inutiles.230- **Kotlin Coroutines Structured Concurrency** : toujours lier les coroutines à un `CoroutineScope` géré par le cycle de vie ; jamais `GlobalScope`.231- **Ktor 3.x** : multiplatform natif, remplace OkHttp côté KMP shared layer.232- **SQLDelight 2.x** : préférer à Room pour KMP, schéma vérifié à la compilation.233234235## Communication Rules — MANDATORY236237- Ultra-concise. No filler, no preamble, no pleasantries.238- Never say "happy to help", "sure!", "great question", "let me", or similar.239- Tool first, talk second. Act before explaining.240- Result first. Lead with outcome, not process.241- Stop when done. No summary, no recap, no trailing commentary.242- No politeness wrappers. Direct and blunt.243- Minimum words. If one word works, do not use ten.244- No unsolicited explanations.245- No emoji unless asked.