Provisioning пользователей в Keycloak и DDD микросервисах Spring Boot
Как организовать provisioning пользователей между PostgreSQL микросервиса и Keycloak в DDD + Hexagonal архитектуре на Java Spring Boot. Transactional Outbox паттерн, event-driven подход, data consistency при сбоях. Лучшие практики от экспертов.
Как правильно организовать provisioning пользователей между базой данных микросервиса и Keycloak в архитектуре DDD + Hexagonal Microservices на Java Spring Boot?
Контекст:
- BFF-сервис оркестрирует запросы фронтенда (Confidential Client, хранит токен).
- Customer Service управляет доменными данными в PostgreSQL.
- Keycloak отвечает за аутентификацию и идентификацию.
Проблема: При регистрации нового клиента нужно создать запись в PostgreSQL customer-service и provision пользователя в Keycloak для входа в систему.
Вопросы:
- Хорошая ли практика обрабатывать provisioning Keycloak внутри Infrastructure Adapter в customer-service?
- Как обеспечить согласованность данных (data consistency), если Keycloak недоступен во время регистрации? Стоит ли использовать Transactional Outbox Pattern?
Рассматривается event-driven подход: публикация события CustomerCreated из customer-service, но неясно, кто должен потреблять его для вызова Admin API Keycloak.
Provisioning пользователей между базой данных микросервиса Customer Service в PostgreSQL и Keycloak в архитектуре DDD + Hexagonal Microservices на Java Spring Boot лучше организовать через event-driven подход с Transactional Outbox Pattern. Это позволяет сохранить запись клиента в PostgreSQL даже при недоступности Keycloak, а provisioning выполнить асинхронно через событие CustomerCreated и отдельный idempotent consumer, вызывающий Admin API Keycloak. Такой метод обеспечивает data consistency без нарушения bounded context и избегает прямой зависимости от внешнего сервиса в Infrastructure Adapter.
Содержание
- Provisioning пользователей в Keycloak: проблемы и лучшие практики
- Интеграция Keycloak Spring Boot в DDD микросервисы
- Обработка регистрации клиента: BFF, Customer Service и Keycloak
- Transactional Outbox паттерн для data consistency
- Event-driven подход: событие CustomerCreated и потребители
- Infrastructure Adapter в Hexagonal Architecture
- Обеспечение согласованности при недоступности Keycloak
- Примеры реализации на Java Spring Boot
- Источники
- Заключение
Provisioning пользователей в Keycloak: проблемы и лучшие практики
Представьте: клиент регистрируется через фронтенд, BFF оркестрирует запрос, Customer Service создает агрегат Customer в PostgreSQL. Но без пользователя в Keycloak вход в систему невозможен. Прямая синхронная интеграция с Keycloak Admin API нарушает принципы микросервисов — coupling растет, транзакции выходят за bounded context, а при сбое Keycloak вся регистрация рушится.
Почему это проблема в DDD? Customer — доменная сущность с lifecycle’ом в своем контексте. Provisioning в Keycloak — это identity provisioning, ближе к Identity Provider. Лучшие практики, описанные Chris Richardson на Microservices.io, рекомендуют eventual consistency через события. А Gary Archer на Stack Overflow подчеркивает: храните в микросервисе только доменные атрибуты (имя, email), а полную идентификацию — в Keycloak для GDPR и простоты.
Коротко: используйте upsert в Keycloak (создать или обновить по ID), idempotent операции и retry. Но не синхронно!
Интеграция Keycloak Spring Boot в DDD микросервисы
Keycloak Spring Boot — отличный выбор для аутентификации в микросервисах. Настройте spring-boot-starter-oauth2-resource-server и keycloak-spring-boot-starter в BFF и сервисах. В application.yml:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://keycloak:8080/realms/myrealm
keycloak:
realm: myrealm
auth-server-url: http://keycloak:8080
В DDD + Hexagonal это значит: аутентификация через port (Security Port), а provisioning — не в домене. BFF как confidential client хранит токен, делегирует регистрацию в Customer Service. Keycloak users API вызывается только асинхронно.
Проблема с пользователями: Keycloak хранит credentials, роли, но не бизнес-данные. Синхронизация через Keycloak Admin Client — норма, но требует осторожности с consistency.
А что если Keycloak недоступен? Здесь вступает магия событий.
Обработка регистрации клиента: BFF, Customer Service и Keycloak
BFF принимает POST /register с данными (email, password). Валидирует, генерирует ID. Затем вызывает Customer Service API: POST /customers.
В Customer Service (Hexagonal):
- Application Service создает Customer Aggregate.
- Repository сохраняет в PostgreSQL.
- Вместо прямого вызова Keycloak — публикует событие.
BFF не ждет provisioning — возвращает 202 Accepted сразу. Клиент проверяет статус по /customers/{id}/status позже.
Почему не в BFF provisioning? BFF — оркестратор, не владелец домена. Customer Service знает о lifecycle клиента.
Эта схема из Microservices.io показывает, как избежать blocking операций.
Transactional Outbox паттерн для data consistency
Да, Transactional Outbox — золотой стандарт для вашего случая. При создании Customer:
@Transactional
public Customer createCustomer(CreateCustomerCommand cmd) {
Customer customer = Customer.create(cmd.getId(), cmd.getEmail(), cmd.getName());
customerRepository.save(customer);
// Outbox в той же транзакции
outboxRepository.save(OutgoingEvent.builder()
.aggregateType("Customer")
.aggregateId(customer.getId())
.eventType("CustomerCreated")
.payload(toJson(customer))
.build());
return customer;
}
Outbox — таблица outbox_events в PostgreSQL с колонками id, aggregate_id, type, payload, processed=false.
Relay (отдельный scheduler, @Scheduled) поллит unprocessed события, публикует в Kafka/RabbitMQ, маркирует processed.
Преимущества: ACID в БД, no distributed transactions. Даже если Kafka down — событие ждет.
Chris Richardson детально разбирает это для микросервисов.
Event-driven подход: событие CustomerCreated и потребители
Кто потребляет CustomerCreated? Не Customer Service! Создайте отдельный сервис: Keycloak Provisioning Service (или модуль).
Consumer (Kafka @KafkaListener):
@KafkaListener(topics = "customer-events")
public void handleCustomerCreated(String payload) {
CustomerCreatedEvent event = fromJson(payload, CustomerCreatedEvent.class);
keycloakAdminClient.users().create(realm, toKeycloakUser(event));
}
Idempotency: проверяйте exists по userId в Keycloak перед create. Используйте if-match ETag в API.
Топики: customer-events.v1. Схемы в Avro для evolution.
Альтернатива: CDC (Debezium на PostgreSQL) — но Outbox проще для DDD событий.
Вопрос: а если consumer упадет? Retry с dead-letter queue.
Infrastructure Adapter в Hexagonal Architecture
Нет, provisioning Keycloak не в Infrastructure Adapter Customer Service. Почему?
- Нарушает SRP: сервис фокусируется на домене.
- Coupling к внешнему API — тестить сложно.
- Gary Archer предупреждает: это дает лишние права, дублирует логику.
Вместо: Port для Domain Events. Adapter публикует в broker. Provisioning — в другом bounded context (Identity Service).
Hexagonal чист: Domain → Application → Ports → Adapters (JPA, Kafka Outbox, REST/Keycloak в consumer).
Просто. Масштабируемо.
Обеспечение согласованности при недоступности Keycloak
Keycloak down? Customer сохраняется в PostgreSQL — регистрация успешна. Consumer retry’ит событие (Kafka retries, Spring Retry).
Стратегия:
- Retry policy: exponential backoff, max 5 attempts.
- Dead letter: ручная обработка.
- Compensating: если нужно, событие CustomerVerificationRequired.
- Monitoring: Prometheus + Grafana на outbox lag.
Eventual consistency — норма в микросервисах. Клиент увидит “pending” статус, пока не provisioned.
Без Outbox пришлось бы rollback всей транзакции — плохо для UX.
PROSELYTE показывает реализацию с Kafka.
Примеры реализации на Java Spring Boot
Зависимости:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>25.0.0</version>
</dependency>
Outbox Entity:
@Entity
@Table(name = "outbox_events")
public class OutboxEvent {
@Id @GeneratedValue private UUID id;
private String aggregateType;
private String aggregateId;
private String eventType;
private String payload;
private boolean processed;
// getters/setters
}
Relay:
@Component
public class OutboxRelay {
@Scheduled(fixedDelay = 5000)
public void publishPendingEvents() {
List<OutboxEvent> pending = outboxRepo.findByProcessedFalse();
for (OutboxEvent event : pending) {
kafkaTemplate.send("customer-events", event.getPayload());
event.setProcessed(true);
outboxRepo.save(event);
}
}
}
Consumer в Provisioning Service — аналогично. Тестируйте с Testcontainers (Keycloak + Kafka + Postgres).
Для полной картины смотрите Piotr’s TechBlog.
Источники
- Stack Overflow — Обсуждение provisioning пользователей между микросервисом и Keycloak от Gary Archer: https://stackoverflow.com/questions/79880139/how-should-user-provisioning-be-handled-between-the-microservice-database-and-ke
- Microservices.io — Transactional Outbox Pattern для reliable messaging в микросервисах от Chris Richardson: https://microservices.io/patterns/data/transactional-outbox.html
- PROSELYTE — Реализация Transactional Outbox с Spring Boot и Kafka: https://proselyte.net/tutorials/spring-boot-kafka-transactional-outbox-pattern/
- Piotr’s TechBlog — Примеры Transactional Outbox в Spring Boot микросервисах: https://piotrminkowski.com/2023/07/12/transactional-outbox-with-spring-boot-and-kafka/
Заключение
Provisioning в Keycloak через Transactional Outbox и event-driven с отдельным consumer — оптимальный путь для DDD + Hexagonal на Spring Boot. Избегайте Infrastructure Adapter в Customer Service, фокусируйтесь на eventual consistency с retry и idempotency. Это масштабируемо, надежно и соответствует best practices от экспертов вроде Richardson и Archer. Начните с PoC — увидите, как упростится жизнь при outage’ах.
В архитектуре с Keycloak рекомендуется централизовать пользовательские данные в Keycloak для упрощения аутентификации Keycloak и соблюдения GDPR, храня в customer-service только доменные атрибуты.
Provisioning Keycloak внутри Infrastructure Adapter customer-service — плохая практика, так как дает сервису лишние привилегии и дублирует логику Keycloak.
Лучше реализовать регистрацию через OAuth flow в Keycloak с верификацией (email), а затем синхронизировать с customer-service upsert-ами.
Для data consistency используйте retry-логику, а не transactional outbox, чтобы избежать усложнения; при недоступности Keycloak клиент повторяет запрос.
Это обеспечивает простоту в spring boot микросервисах без event-driven overhead.
Transactional outbox паттерн идеален для provisioning пользователей в DDD микросервисах: при создании Customer в customer-service сохраняйте бизнес-данные и событие CustomerCreated в outbox-таблице в одной транзакции PostgreSQL.
Relay-процесс публикует событие в Kafka, а consumer вызывает Keycloak Admin API для создания пользователя.
Это гарантирует consistency даже при недоступности Keycloak — событие retry’ится асинхронно.
Не размещайте provisioning в Infrastructure Adapter customer-service; выносите в отдельный сервис.
В Keycloak Spring Boot реализуйте idempotent consumer по aggregate ID для at-least-once семантики.

