Seguro que te ha pasado: empiezas un proyecto móvil con toda la ilusión, pero a los pocos meses el código se ha convertido en un auténtico laberinto donde tocar una línea en la UI rompe la base de datos. La arquitectura limpia o Clean Architecture llega precisamente para rescatarnos de ese caos, permitiendo que el software sea robusto y, sobre todo, que no nos den ganas de borrarlo todo y empezar de cero cada vez que el cliente pide un cambio.
No se trata de seguir una receta rígida de cocina, sino de adoptar una filosofía de diseño. El objetivo es que el núcleo de tu negocio no dependa de si usas una base de datos SQLite, una API REST o si decides cambiar Jetpack Compose por otra tecnología. Al final del día, queremos que el código sea fácil de testear y mantener, evitando que las clases se inflen como globos y se vuelvan imposibles de gestionar.
El andamiaje de la arquitectura: Las capas fundamentales
Para que esto funcione, necesitamos dividir la aplicación en sectores con responsabilidades muy claras. Aunque el número de capas puede variar según el tamaño del equipo o el proyecto, existen tres pilares que casi siempre están presentes. Primero tenemos la capa de presentación, que es la cara visible. Aquí es donde viven las Activities, Fragments y los ViewModels (en MVVM) o Presenters (en MVP), encargándose exclusivamente de mostrar los datos y capturar los gestos del usuario.
Luego bajamos al corazón del sistema: la capa de dominio. Esta es la zona sagrada porque contiene la lógica de negocio pura y las entidades. Aquí es donde definimos los casos de uso o Interactors, que son básicamente las acciones que el usuario puede disparar en la app. Lo mejor de esta capa es que es totalmente agnóstica; no sabe nada de Android ni de iOS, solo de las reglas del negocio.
Finalmente encontramos la capa de datos. Su misión es gestionar el flujo de información, ya sea recuperando datos de una caché local o haciendo una petición a un servidor. Para evitar que el resto de la app se vuelva loca con el origen de los datos, se suele implementar el patrón de repositorio, que actúa como un mediador inteligente entre las distintas fuentes de datos.
Estrategias de organización y modularización
A la hora de aterrizar esto en el código, hay varias formas de organizar los módulos. La opción más sencilla es meter todo en un único módulo de aplicación. Es rápido de configurar, pero tiene un peligro enorme: es muy fácil saltarse las reglas y acabar con una dependencia prohibida, lo que convierte el proyecto en un plato de espaguetis a medida que crece.
Para mejorar esto, podemos optar por dividir el proyecto por capas, creando un módulo para datos, otro para dominio y otro para presentación. Esto da mucha claridad visual, pero si la app es muy grande, seguiríamos teniendo módulos gigantescos. Por eso, la opción más profesional suele ser la separación por funcionalidades (features). En este enfoque, cada característica de la app (por ejemplo, «Carrito de Compras» o «Perfil de Usuario») tiene su propia estructura interna de capas. Esto permite que el desarrollo sea independiente y mucho más escalable, facilitando además la implementación de lazy loading para que la app vuele.
El secreto del éxito: La Inversión de Dependencias
Si hay algo que suele dar dolores de cabeza es la interacción entre capas. Lo lógico sería pensar que el flujo es lineal: la UI llama al caso de uso, este al repositorio y este al framework (como Room o Retrofit). Pero si lo hacemos así, la capa interna dependería de la externa, y si cambiamos la base de datos, tendríamos que modificar todo el camino. Aquí es donde entra la inversión de dependencias, la famosa «D» de los principios SOLID.
La clave está en que los módulos de alto nivel no dependan de los de bajo nivel, sino que ambos dependan de abstracciones. En lugar de que la capa de datos instancie directamente una base de datos, define una interfaz (un contrato) en el dominio. La implementación real de esa interfaz se queda en la capa de framework. Así, el repositorio solo sabe que existe un método para guardar datos, pero no le importa cómo se haga. Esto es oro puro para hacer unit tests, ya que podemos sustituir la base de datos real por un fake en un abrir y cerrar de ojos.
Adaptación a entornos modernos y multiplataforma
Hoy en día, con el auge de Kotlin Multiplatform (KMP), Clean Architecture se vuelve aún más vital. Al compartir la lógica de negocio entre Android e iOS, la capa de dominio y de datos residen en el commonMain, mientras que la presentación se gestiona con Compose Multiplatform. Para manejar las particularidades de cada sistema operativo, se utilizan los mecanismos de expect/actual, permitiendo que la arquitectura se mantenga limpia mientras se aprovechan las optimizaciones nativas de cada plataforma.
En el mundo del front-end o aplicaciones móviles muy dinámicas, también se habla del patrón contenedor. Este consiste en encapsular la lógica de obtención de datos y el estado en un componente principal (el contenedor), dejando que los componentes hijos sean puramente visuales. Esto, sumado a los adaptadores de datos, asegura que la información que viene de una API externa se transforme en un formato cómodo para la UI, evitando que los modelos de la base de datos se filtren hasta la pantalla.
Herramientas y desafíos en el camino
Para que todo este sistema de inyección de dependencias no se vuelva una pesadilla manual, contamos con herramientas como Dagger o Koin en Android, y Swinject en iOS. Estas librerías se encargan de suministrar las implementaciones correctas en el momento justo. Sin embargo, no todo es color de rosa; implementar Clean Architecture conlleva una curva de aprendizaje pronunciada y una inversión de tiempo inicial considerable debido a la cantidad de archivos y clases que hay que crear.
Es fundamental no caer en el purismo extremo. A veces, crear un modelo de datos diferente para cada capa y hacer transformaciones constantes puede generar una sobrecarga de código repetitivo que no aporta valor real en proyectos pequeños. Lo ideal es encontrar un equilibrio pragmático: aplicar la separación de responsabilidades donde realmente impacte en la mantenibilidad y no complicar la estructura si la app es un CRUD sencillo de tres entidades. Comparte la información y más usuarios conocerán del tema.