← Назад к вопросам

На каких технологиях был реализован модуль уведомлений

1.3 Junior🔥 111 комментариев
#Soft Skills и карьера#Брокеры сообщений

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Модуль уведомлений: технологии и реализация

Это вопрос о конкретной системе (видимо, интервьюируемого проекта). Я дам стандартный ответ на уровне expert'а, который покрывает современные подходы и лучшие практики.

Типовая архитектура системы уведомлений

Полнофункциональный модуль уведомлений обычно состоит из:

┌─────────────────────────────────────────┐
│         APPLICATION LAYER              │
│  (Сервис отправки уведомлений)         │
└────────────────────┬────────────────────┘
                     │
         ┌───────────┼───────────┐
         ▼           ▼           ▼
  ┌────────────┐ ┌────────┐ ┌─────────┐
  │ Message    │ │ Routing│ │ Template│
  │ Broker     │ │ Engine │ │ Engine  │
  │ (RabbitMQ)│ │        │ │         │
  └────────────┘ └────────┘ └─────────┘
         │
    ┌────┴──────┬──────────┬──────────┐
    ▼           ▼          ▼          ▼
  Email     Push Notif   SMS        Webhook
  Handler   Handler      Handler    Handler

Основные компоненты

1. Message Broker

Обычно используются:

// ✅ RabbitMQ — популярный выбор
// Достоинства:
// - Надёжная доставка (acknowledgements)
// - Маршрутизация (exchanges, queues)
// - Персистентность

RabbitTemplate template = rabbitTemplate(connectionFactory);
template.convertAndSend(
    "notifications.exchange",
    "email.routing.key",
    emailNotification
);

// ✅ Apache Kafka — для больших объёмов
// Достоинства:
// - Горизонтальная масштабируемость
// - Наличие истории (retention)
// - Высокий throughput

kafkaTemplate.send(
    "notifications-topic",
    emailNotification
);

// ✅ Redis Streams — для быстрого перетоков
redisTemplate.opsForStream().add(
    "notifications-stream",
    Map.of("id", notification.getId())
);

2. Сервис уведомлений (Application Layer)

@Service
public class NotificationService {
    
    private final NotificationRepository notificationRepo;
    private final MessageBroker messageBroker;
    private final NotificationTemplateEngine templateEngine;
    
    @Transactional
    public void sendNotification(NotificationRequest request) {
        // 1. Сохраняем в БД (для истории и retry)
        Notification notification = new Notification();
        notification.setUserId(request.getUserId());
        notification.setType(request.getType());
        notification.setStatus(NotificationStatus.PENDING);
        notification.setCreatedAt(Instant.now(ZoneId.of("UTC")));
        
        Notification saved = notificationRepo.save(notification);
        
        // 2. Отправляем в message broker
        messageBroker.publish(
            "notifications.exchange",
            request.getType().getRoutingKey(),
            new NotificationEvent(saved)
        );
    }
    
    @Transactional
    public void markAsSent(UUID notificationId) {
        Notification notification = notificationRepo.findById(notificationId)
            .orElseThrow();
        notification.setStatus(NotificationStatus.SENT);
        notification.setSentAt(Instant.now(ZoneId.of("UTC")));
        notificationRepo.save(notification);
    }
}

3. Template Engine

// ✅ FreeMarker — для шаблонов
@Service
public class TemplateEngine {
    
    private final Configuration freemarkerConfig;
    
    public String render(String templateName, Map<String, Object> data) 
            throws IOException, TemplateException {
        Template template = freemarkerConfig.getTemplate(templateName);
        StringWriter output = new StringWriter();
        template.process(data, output);
        return output.toString();
    }
}

// Шаблон: templates/email_welcome.ftl
// <html>
//   <body>
//     <h1>Привет, ${userName}!</h1>
//     <p>Код подтверждения: ${confirmationCode}</p>
//   </body>
// </html>

// ✅ Thymeleaf — альтернатива
@Autowired
private ThymeleafTemplateEngine thymeleafEngine;

4. Email Handler

@Service
public class EmailNotificationHandler {
    
    private final JavaMailSender mailSender;
    private final TemplateEngine templateEngine;
    
    @RabbitListener(queues = "email-notifications-queue")
    public void handleEmailNotification(NotificationEvent event) {
        try {
            String htmlContent = templateEngine.render(
                "email_" + event.getType().toString().toLowerCase() + ".ftl",
                event.getContext()
            );
            
            SimpleMailMessage message = new SimpleMailMessage();
            message.setTo(event.getRecipient());
            message.setSubject(event.getSubject());
            message.setText(htmlContent);
            
            mailSender.send(message);
            
            // Обновляем статус
            notificationService.markAsSent(event.getId());
        } catch (Exception e) {
            handleError(event, e);
        }
    }
}

// Или с Spring Mail Mime Message (для HTML)
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(
    mimeMessage, 
    true,  // multipart
    "UTF-8"
);
helper.setTo(event.getRecipient());
helper.setSubject(event.getSubject());
helper.setText(htmlContent, true);  // true = HTML
mailSender.send(mimeMessage);

5. Push Notification Handler (Firebase)

@Service
public class PushNotificationHandler {
    
    private final FirebaseMessaging firebaseMessaging;
    
    @RabbitListener(queues = "push-notifications-queue")
    public void handlePushNotification(NotificationEvent event) {
        Notification notification = Notification.builder()
            .setTitle(event.getTitle())
            .setBody(event.getBody())
            .putData("notification_id", event.getId().toString())
            .putData("click_action", event.getClickAction())
            .build();
        
        // Отправляем конкретному девайсу по токену
        String deviceToken = event.getDeviceToken();
        
        try {
            String response = firebaseMessaging.send(
                Message.builder()
                    .setToken(deviceToken)
                    .setNotification(notification)
                    .build()
            );
            
            logger.info("Push sent: " + response);
            notificationService.markAsSent(event.getId());
        } catch (FirebaseMessagingException e) {
            // Токен невалиден, удаляем
            if (e.getMessagingErrorCode() == MessagingErrorCode.THIRD_PARTY_AUTH_ERROR) {
                userService.removeDeviceToken(event.getUserId());
            }
        }
    }
}

6. SMS Handler (Twilio)

@Service
public class SmsNotificationHandler {
    
    private final TwilioRestClient twilio;
    
    @RabbitListener(queues = "sms-notifications-queue")
    public void handleSmsNotification(NotificationEvent event) {
        try {
            Message message = Message.creator(
                new PhoneNumber("+" + event.getPhoneNumber()),  // To
                new PhoneNumber("+1234567890"),                // From
                event.getMessage()
            )
            .create();
            
            logger.info("SMS sent: " + message.getSid());
            notificationService.markAsSent(event.getId());
        } catch (TwilioRestException e) {
            logger.error("SMS failed: " + e.getErrorMessage());
            handleRetry(event);
        }
    }
}

7. Webhook Handler (для интеграций)

@Service
public class WebhookNotificationHandler {
    
    private final RestTemplate restTemplate;
    
    @RabbitListener(queues = "webhook-notifications-queue")
    public void handleWebhookNotification(NotificationEvent event) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("X-Webhook-Secret", generateSignature(event));
            
            HttpEntity<WebhookPayload> request = new HttpEntity<>(
                new WebhookPayload(event),
                headers
            );
            
            ResponseEntity<String> response = restTemplate.postForEntity(
                event.getWebhookUrl(),
                request,
                String.class
            );
            
            if (response.getStatusCode().is2xxSuccessful()) {
                notificationService.markAsSent(event.getId());
            } else {
                handleRetry(event);
            }
        } catch (Exception e) {
            handleError(event, e);
        }
    }
}

Retry механизм

@Service
public class NotificationRetryService {
    
    // Переотправляем не доставленные уведомления
    @Scheduled(fixedRate = 60000)  // Каждую минуту
    public void retryFailedNotifications() {
        List<Notification> failed = notificationRepo
            .findByStatusAndCreatedAtAfter(
                NotificationStatus.FAILED,
                Instant.now(ZoneId.of("UTC")).minus(Duration.ofHours(24))
            );
        
        for (Notification notification : failed) {
            if (notification.getRetryCount() < 3) {
                notification.setRetryCount(notification.getRetryCount() + 1);
                messageBroker.republish(notification);
            } else {
                notification.setStatus(NotificationStatus.PERMANENTLY_FAILED);
                notificationRepo.save(notification);
            }
        }
    }
}

Мониторинг и логирование

@Aspect
@Service
public class NotificationMonitoring {
    
    @Around("@annotation(Monitored)")
    public Object monitor(ProceedingJoinPoint point) throws Throwable {
        long start = System.currentTimeMillis();
        String method = point.getSignature().getName();
        
        try {
            Object result = point.proceed();
            long duration = System.currentTimeMillis() - start;
            
            logger.info("Notification {} succeeded in {}ms", method, duration);
            metrics.recordSuccess(method, duration);
            
            return result;
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - start;
            logger.error("Notification {} failed after {}ms: {}", 
                method, duration, e.getMessage());
            metrics.recordFailure(method, duration, e);
            throw e;
        }
    }
}

Конфигурация RabbitMQ

@Configuration
public class NotificationQueueConfig {
    
    // Exchanges
    @Bean
    public TopicExchange notificationsExchange() {
        return new TopicExchange("notifications.exchange", true, false);
    }
    
    // Queues
    @Bean
    public Queue emailQueue() {
        return new Queue("email-notifications-queue", true);
    }
    
    @Bean
    public Queue pushQueue() {
        return new Queue("push-notifications-queue", true);
    }
    
    // Bindings
    @Bean
    public Binding emailBinding(Queue emailQueue, TopicExchange exchange) {
        return BindingBuilder.bind(emailQueue)
            .to(exchange)
            .with("email.*");
    }
}

Типовая стек технологий

СлойТехнологияПричина
Message BrokerRabbitMQ / KafkaНадёжность, масштабируемость
ApplicationSpring BootEnterprise стандарт
TemplatesFreeMarker / ThymeleafГибкость, переиспользование
EmailSpring Mail / SendGrid / AWS SESНадёжность, масштабируемость
PushFirebase / OneSignalКросс-платформа
SMSTwilio / NexmoГлобальное покрытие
DatabasePostgreSQLИстория уведомлений
Async@Async / CompletableFutureНеблокирующая обработка
Scheduling@Scheduled / QuartzПериодические проверки
MonitoringPrometheus / GrafanaВидимость

Практические выводы

Message Broker — основа (асинхронность) ✅ Разные каналы — Email, Push, SMS, Webhook ✅ Template Engine — для гибких сообщений ✅ Retry механизм — для надёжности ✅ Мониторинг — для видимости ✅ Persistence — для истории и отладки

Правильный ответ на интервью: "Я бы реализовал модуль уведомлений с использованием RabbitMQ как message broker для асинхронной обработки, Spring Boot для приложения, FreeMarker для шаблонов, и отдельные обработчики для Email (Spring Mail), Push (Firebase), SMS (Twilio) и Webhook. Всё с retry механизмом, сохранением истории в PostgreSQL и мониторингом через Prometheus."