Esse projeto foi criado com o objetivo de estudar e demonstrar diferentes padroes de design.
O AlgaComments são dois serviços que conversam entre si: uma API de comentários (comment-service) e uma API de moderação de texto (moderation-service). A regra: um comentário só pode ser salvo depois que o serviço de moderação aprovar o texto.
A integração entre os dois é síncrona: o comment-service faz uma chamada HTTP, espera a resposta e decide o que fazer. Comecei por aí para sentir os problemas que o acoplamento temporal traz e depois pensar em como resolveria.
A regra de negócio vive no CommentService. Antes de salvar qualquer coisa, ele pergunta para a moderação se o texto está liberado:
public Comment create(CommentInput input) {
if (input.text() == null || input.author() == null) {
throw new BadInputException("Neither text nor author can be null.");
}
Comment comment = input.toModel();
if (!moderationService.validateText(new ModerationInput(comment.getText(), comment.getId()))) {
throw new ModerationException(comment.getText());
}
return commentRepository.save(comment);
}
O comment-service não sabe como a moderação decide se um texto é válido. Ele conhece um contrato: manda um texto, recebe um approved. Os dois serviços evoluem cada um no seu ritmo.
Em vez de montar a requisição na mão, usei a interface HTTP declarativa do Spring 6. Você descreve a chamada numa interface e o framework gera a implementação:
@HttpExchange("/api/moderate")
public interface ModerationClient {
@PostExchange
ModerationOutput validateText(@RequestBody ModerationInput input);
}
A implementação acontece num @Configuration, onde um RestClient é embrulhado num proxy:
@Bean
public ModerationClient moderationClient(RestClientFactory factory) {
RestClient restClient = factory.monitorClient();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory proxy = HttpServiceProxyFactory.builderFor(adapter).build();
return proxy.createClient(ModerationClient.class);
}
Mais limpo que escrever a chamada à mão e sem biblioteca externa tipo Feign.
Se a moderação está fora do ar, lenta ou recusando conexão, o fluxo de criar comentário quebra junto. Para enxergar esse problema, separei os erros de integração por tipo:
public boolean validateText(ModerationInput input) {
try {
return moderationClient.validateText(input).approved();
} catch (RuntimeException e) {
log.error("Error sending comment to client \n{}", e.getMessage());
if (isTimeout(e)) {
throw new IntegrationTimeoutException("Moderation service timeout");
}
throw new IntegrationException("Error calling moderation service");
}
}
Distinguir timeout de erro genérico importa: um timeout vale a pena tentar de novo, um 400 não. Para testar, deixei um Thread.sleep comentado dentro do validador da moderação. Descomentando, simulo a moderação travada e vejo o comment-service estourar IntegrationTimeoutException.
A moderação compara o texto contra uma lista de palavras proibidas, usando regex com word boundary (\b) para não pegar pedaços de palavra por acidente:
for (String word : ForbiddenWordsList.WORDS) {
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(word) + "\\b");
if (pattern.matcher(input).find()) {
return false; // reprovado
}
}
return true;
O Pattern.quote evita que uma palavra com caractere especial vire uma regex maluca.
A evolução natural é trocar a chamada síncrona por mensageria como RabbitMQ: o comentário entra numa fila, é moderado de forma assíncrona e só então publicado. O sistema para de cair junto com a moderação e aguenta picos de carga sem perder requisição. Foi esse caminho assíncrono que explorei depois no AlgaSensors.