La creación de una service class en Kotlin permite orquestar varios repositorios y centralizar la lógica de negocio de una aplicación, en este caso un simulador de correos. Si estás aprendiendo backend con Kotlin o vienes de frameworks como Spring Boot, este patrón te resultará familiar y útil para estructurar tu código de forma profesional.
¿Qué hace una service class en un simulador de email?
Una service class coordina dos o más repositorios y aplica reglas de negocio sobre los datos que orquesta. En el ejemplo, la InboxService une el UserRepository y el InMemoryEmailRepository para resolver flujos como registro, login, envío y recepción de correos [01:05].
Este patrón es muy común en aplicaciones backend desarrolladas con Spring Boot, donde Java y Kotlin se usan para construir APIs que manejan datos provenientes de múltiples fuentes. La clave está en separar responsabilidades: el repositorio guarda y recupera datos, y el servicio decide qué hacer con ellos.
¿Qué es una service class? Es una clase que contiene la lógica de negocio de una aplicación y orquesta uno o varios repositorios para resolver casos de uso completos como registrar, autenticar o enviar información.
¿Por qué definir excepciones personalizadas en Kotlin?
Antes de escribir la lógica, conviene crear excepciones propias que describan con precisión qué falló. En el simulador se definen tres clases que heredan de RuntimeException [02:30]:
UserNotFoundException: se lanza cuando el usuario no existe en el repositorio.
AuthenticationException: se lanza cuando las credenciales son incorrectas.
UserAlreadyExistsException: se lanza al intentar registrar un usuario ya existente.
Estas excepciones son el equivalente conceptual a los errores que ves en una web cuando aparece un 404 Not Found o un mensaje genérico tipo usuario o contraseña incorrectos. Al heredar de RuntimeException, aprovechas la herencia de Kotlin para integrarlas naturalmente al manejo de errores del lenguaje.
¿Cómo implementar registro y login con manejo de errores?
El método de registro recibe un username y un password, ambos de tipo String. Antes de crear el usuario, se consulta el repositorio con findByUsername. Si el resultado es distinto de nulo, significa que el usuario ya existe y se lanza UserAlreadyExistsException [04:50].
El método de login es más interesante porque combina dos validaciones en pocas líneas:
- Buscar el usuario con
findUserByName(username). Si el resultado es nulo, lanzar UserNotFoundException usando el operador Elvis (?:).
- Validar la contraseña con
authenticatePassword. Si la contraseña no coincide, lanzar AuthenticationException.
- Si todo sale bien, retornar el objeto
user para usarlo en operaciones posteriores.
¿Qué es el operador Elvis en Kotlin? Es el operador ?: que permite ejecutar una acción alternativa cuando una expresión es nula. En este caso se usa para lanzar una excepción si el repositorio no encuentra al usuario, deteniendo la ejecución de forma elegante.
Esta forma de escribir condiciones convierte líneas que en otros lenguajes serían verbosas en expresiones compactas. Y aquí viene lo interesante: la lógica de validación queda en una sola línea por caso, lo que hace tu código más legible.
¿Cómo enviar un email entre dos usuarios?
La función sendEmail recibe el user que envía, un toUsername, un subject y un body. Con esos datos construye un objeto email donde el owner es el user.id del remitente y el folder se asigna como Folder.SENT para mantener coherencia con la acción [09:15].
Después el repositorio guarda ese email en la carpeta de enviados. Pero hay un paso adicional: si el destinatario existe en el userRepository, se hace una copia del email usando la función copy, se cambia el folder a INBOX y el owner al ID del receptor. Así el mismo correo aparece en la bandeja de salida del remitente y en la bandeja de entrada del destinatario.
En una aplicación real esto se resolvería con tablas relacionales en una base de datos. Para un simulador, una sola lista filtrada por owner y folder funciona bastante bien.
¿Cómo simular la recepción de un email sin conexión?
La función receiveEmail simula que llega un correo desde fuera del sistema. Recibe el user destinatario, un from que indica el origen, un subject y un body [13:40]. Con esos datos arma un objeto email donde el owner es el ID del usuario receptor y el folder queda como INBOX por defecto.
Finalmente, emailsRepository.save(email) persiste el correo en la lista. Como no hay conexión real a internet, esta función reemplaza al servicio externo que normalmente entregaría el mensaje.
Conceptos clave que aparecen en el flujo
- Inyección de dependencias por constructor: el
InboxService recibe ambos repositorios como private val para usarlos en todos sus métodos [01:30].
- Función copy de data class: permite duplicar un objeto cambiando solo algunos campos, ideal para reutilizar el mismo email con distinto folder y owner [11:20].
- Herencia de RuntimeException: las excepciones personalizadas heredan de la clase base de Kotlin para integrarse al manejo estándar de errores [03:10].
- Operador Elvis (?:): ejecuta una expresión alternativa cuando el valor a la izquierda es nulo, útil para lanzar excepciones en una línea [07:25].
¿Cómo organizarías tú la lógica de envío entre múltiples carpetas? Cuéntame en los comentarios qué cambiarías de esta implementación.