Librería modular y extensible para envío de notificaciones construida con:
- Java 21
- Arquitectura Hexagonal (Ports & Adapters)
- Sealed Classes
- Records
- Estrategia de Reintentos (Retry)
- Notificaciones Asíncronas con CompletableFuture
- Logging estructurado con SLF4J
- Patrón Builder
- Soporte multi-proveedor (SMS, Email, Push)
El proyecto sigue el enfoque de Arquitectura Hexagonal, separando claramente responsabilidades:
notifications
├── notifications-app # Proyecto consumidor (uso de la librería)
│ ├── src/main/java
│ ├── src/main/resources
│ └── Dockerfile
│
├── notifications-lib # Librería principal
│ ├── application
│ ├── domain
│ ├── infrastructure
│ ├── bootstrap
│ └── test
│
└── pom.xml # Parent POM (multi-módulo)
- Modelos de negocio (
record) - Validaciones
- Excepciones selladas (
sealed) - Resultado de notificación (
NotificationResult)
- Orquestación del envío (
DefaultNotificationSender) - Estrategia de reintentos (
SimpleRetryStrategy) - Soporte asíncrono (
CompletableFuture) - Registro de canales (
ChannelsRegistry)
- Adaptadores hacia proveedores externos:
- Twilio (SMS)
- Firebase (Push)
- SendGrid (Email)
- Mailgun (Email)
- Configuración mediante patrón Builder (
NotificationSenderBuilder)
TwilioSmsAdapter
SendGridEmailAdapterMailgunEmailAdapter
FirebasePushAdapter
- Fail-Fast (validación de configuración en constructor)
- Separación clara de responsabilidades
- Retry manejado en capa de aplicación
- Infraestructura desacoplada del dominio
- Logging estructurado
- Extensible (Open/Closed Principle)
- Soporte síncrono y asíncrono
- Alta cohesión y bajo acoplamiento
NotificationSender sender = NotificationSenderBuilder.builder()
.withSms(new TwilioSmsAdapter("SID", "TOKEN"))
.withEmail(new SendGridEmailAdapter("SENDGRID_API_KEY"))
.withPush(new FirebasePushAdapter("FIREBASE_SERVICE_KEY"))
.retries(3)
.build();
sender.send(new SmsNotification("+51999999999", "Mensaje SMS"));
sender.send(new EmailNotification(
"joseguzman@oh.com",
"Aceptación",
"Ha sido aceptado"
));
sender.send(new PushNotification(
"device-token",
"Hello via PUSH!",
"Ha sido aceptado"
));NotificationSender sender = NotificationSenderBuilder.builder()
.withSms(new TwilioSmsAdapter("SID", "TOKEN"))
.withEmail(new SendGridEmailAdapter("SENDGRID_API_KEY"))
.withPush(new FirebasePushAdapter("FIREBASE_SERVICE_KEY"))
.retries(3)
.build();
CompletableFuture<NotificationResult> future =
sender.sendAsync(
new SmsNotification("+51999999999", "Hello via SMS!")
);
future.thenAccept(result ->
System.out.println("Resultado async: " + result)
);
future.join();El mecanismo de reintentos se implementa mediante la clase:
SimpleRetryStrategy
Esta estrategia permite ejecutar una operación múltiples veces en caso de error, hasta alcanzar un número máximo de intentos configurado.
La lógica de reintento se encuentra en la capa de aplicación, garantizando que los adaptadores de infraestructura permanezcan enfocados únicamente en la comunicación con el proveedor externo.
Beneficios:
- Separación clara de responsabilidades
- Mayor testabilidad
- Reutilización de la estrategia
- Posibilidad de implementar nuevas estrategias (por ejemplo, Exponential Backoff)
La arquitectura está diseñada para ser extensible sin modificar el código existente (Open/Closed Principle).
Para agregar un nuevo canal, por ejemplo WhatsApp, solo debes seguir estos pasos:
Definir un nuevo record que implemente la interfaz Notification.
package com.notifications.lib.domain.model;
public record WhatsAppNotification(
String phoneNumber,
String message
) implements Notification {
public WhatsAppNotification {
if (phoneNumber == null || phoneNumber.isBlank()) {
throw new ValidationException("Phone number cannot be empty");
}
if (message == null || message.isBlank()) {
throw new ValidationException("Message cannot be empty");
}
}
}Implementar un proveedor que use el puerto NotificationProviderPort.
package com.notifications.lib.infrastructure.whatsapp;
public class WhatsAppAdapter
implements NotificationProviderPort<WhatsAppNotification> {
@Override
public NotificationResult send(WhatsAppNotification notification) {
System.out.println("Sending WhatsApp to "
+ notification.phoneNumber());
return NotificationResult.ok();
}
}Extender el NotificationSenderBuilder:
public NotificationSenderBuilder withWhatsApp(
NotificationProviderPort<WhatsAppNotification> provider
) {
registry.register(WhatsAppNotification.class, provider);
return this;
}NotificationSender sender = NotificationSenderBuilder.builder()
.withWhatsApp(new WhatsAppAdapter())
.build();
sender.send(new WhatsAppNotification(
"+51999999999",
"Hola desde WhatsApp"
));Para ejecutar las pruebas unitarias:
mvn clean test
Para generar el artefacto .jar e instalarlo en el repositorio local de
Maven:
mvn clean install
Esto permitirá que otros proyectos puedan utilizar la librería como dependencia.
Si estás utilizando el módulo notifications-app, puedes construir una
imagen Docker para ejecutar la aplicación en un contenedor.
docker build -t notifications-app .
docker run notifications-app
- Implementar Exponential Backoff en la estrategia de reintentos
- Agregar Circuit Breaker
- Integración con Micrometer para métricas
- Configuración de timeouts por proveedor
- Soporte multi-provider con fallback automático
- Observabilidad avanzada
Jose Guzman Gonzales
Senior Java Developer