Este artigo faz parte da serie “Jornada Oracle FreeStack”. No artigo anterior, resolvemos o armazenamento de midia com OCI Object Storage. Hoje, vamos desacoplar nossa aplicacao com eventos usando o OCI Notifications.
Dando continuidade a nossa Jornada Oracle FreeStack, apos garantir a seguranca com o OCI Vault (Phase 3) e o armazenamento de midia com o Object Storage (Phase 4), chegamos a um ponto crucial de qualquer arquitetura moderna: o desacoplamento.
Neste artigo, vamos transformar nosso backend em uma arquitetura orientada a eventos (Event-Driven Architecture - EDA) usando o OCI Notifications (ONS) - o servico pub/sub nativo da OCI que e 100% Always Free (1 milhao de notificacoes HTTPS/mes, 1000 emails/mes).
O Cenario
Em sistemas de alto desempenho, nao queremos que o usuario espere por tarefas secundarias (como indexacao de busca ou processamento de imagem) durante o processo de criacao de um artigo. O backend deve persistir o dado no Oracle 26ai, disparar um evento e responder imediatamente. O processamento pesado acontece de forma assincrona.
OCI Notifications: O Pub/Sub do Free Tier
O OCI Notifications (ONS) e um servico de mensageria pub/sub totalmente gerenciado. Ele nao exige gerenciamento de clusters ou particoes - usa a mesma autenticacao OCI (API Key ou Instance Principals) que ja configuramos para o Vault.
Para o nosso cenario (notificar sistemas quando um artigo e criado), o ONS e mais do que suficiente - e custa zero.
Infraestrutura: Provisionando via OCI CLI
Todo o provisionamento foi feito via CLI, sem necessidade de Console.
1. Recriar o Autonomous Database (se necessario)
Se o banco anterior foi excluido (como aconteceu conosco), criamos um novo via CLI com um unico comando:
TENANCY=$(cat ~/.oci/config | grep tenancy | head -1 | cut -d= -f2 | tr -d ' ')
oci db autonomous-database create \
--compartment-id "$TENANCY" \
--db-name "FreeStackDB" \
--display-name "FreeStack-Lab-DB" \
--admin-password "<SENHA_FORTE_AQUI>" \
--db-workload OLTP \
--data-storage-size-in-tbs 1 \
--is-free-tier true \
--license-model LICENSE_INCLUDED \
--db-version "23ai"
Aguarde o provisionamento (aproximadamente 2 minutos):
ATP_ID="ocid1.autonomousdatabase.oc1.sa-saopaulo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
oci db autonomous-database get \
--autonomous-database-id "$ATP_ID" \
--query 'data."lifecycle-state"'
Quando retornar "AVAILABLE", atualize a senha no Vault e baixe o wallet:
# Atualizar a senha no Vault (cria uma nova versao do segredo)
oci vault secret update-base64 \
--secret-id "ocid1.vaultsecret.oc1.sa-saopaulo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
--secret-content-content "$(echo -n '<SENHA_FORTE_AQUI>' | base64)" \
--secret-content-stage CURRENT \
--force
# Baixar o wallet mTLS
oci db autonomous-database generate-wallet \
--autonomous-database-id "$ATP_ID" \
--password "<WALLET_PASSWORD>" \
--file /tmp/wallet.zip
# Extrair para o projeto
WALLET_DIR="src/main/resources/wallet"
rm -rf "$WALLET_DIR"/*
unzip -o /tmp/wallet.zip -d "$WALLET_DIR"
Importante: Apos extrair o wallet, corrija o sqlnet.ora com o caminho absoluto do diretorio:
# Antes (nao funciona):
WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="?/network/admin")))
# Depois (caminho absoluto do seu projeto):
WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="/caminho/absoluto/para/seu/projeto/src/main/resources/wallet")))
2. Criar o Topic ONS
oci ons topic create \
--compartment-id "$TENANCY" \
--name "articles-events" \
--description "Article lifecycle events for FreeStack"
Resultado:
"topic-id": "ocid1.onstopic.oc1.sa-saopaulo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
"lifecycle-state": "ACTIVE"
3. Adicionar Subscriptions (opcional)
Para receber notificacoes por email quando um artigo for criado:
oci ons subscription create \
--compartment-id "$TENANCY" \
--topic-id "ocid1.onstopic.oc1.sa-saopaulo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
--protocol "EMAIL" \
--subscription-endpoint "[email protected]"
O ONS suporta protocolos: EMAIL, HTTPS, SLACK, PAGERDUTY, e ORACLE_FUNCTIONS.
Implementacao: A Anatomia da Feature
1. Dependencia
Adicionamos o SDK nativo do ONS:
<dependency>
<groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-ons</artifactId>
</dependency>
O oci-java-sdk-ons ja esta no BOM do OCI SDK (oci-java-sdk-bom) que configuramos na Phase 3.
2. O Contrato do Evento (Records)
Em vez de usar Map<String, Object> sem tipagem, definimos records Java para garantir type safety e autodocumentacao:
package com.freestack.infra.notification;
import java.time.Instant;
public record NotificationEvent(
String eventType,
Object data,
Instant timestamp) {
public static NotificationEvent of(String eventType, Object data) {
return new NotificationEvent(eventType, data, Instant.now());
}
}
O payload especifico do dominio tambem e um record:
package com.freestack.article.dto;
import java.time.Instant;
public record ArticleEvent(
Long id,
String title,
String author,
Instant createdAt) {
}
3. O Servico de Notificacao
Criamos um servico CDI que encapsula a comunicacao com o OCI Notifications via SDK:
package com.freestack.infra.notification;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.oracle.bmc.ConfigFileReader;
import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider;
import com.oracle.bmc.ons.NotificationDataPlaneClient;
import com.oracle.bmc.ons.model.MessageDetails;
import com.oracle.bmc.ons.requests.PublishMessageRequest;
import com.oracle.bmc.ons.requests.PublishMessageRequest.MessageType;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import java.time.Instant;
@ApplicationScoped
public class OciNotificationService {
private static final Logger LOG = Logger.getLogger(OciNotificationService.class);
@ConfigProperty(name = "oci.notification.topic-id")
String topicId;
@ConfigProperty(name = "oci.auth.instance-principal", defaultValue = "false")
boolean useInstancePrincipal;
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
public void publishEvent(NotificationEvent event) {
LOG.infof("Publishing event [%s] to ONS topic: %s",
event.eventType(), topicId);
try (NotificationDataPlaneClient client = createClient()) {
String eventJson = objectMapper.writeValueAsString(event);
String jsonBody = objectMapper.writeValueAsString(
new OnsMessageBody(eventJson));
MessageDetails messageDetails = MessageDetails.builder()
.title(event.eventType())
.body(jsonBody)
.build();
PublishMessageRequest request = PublishMessageRequest.builder()
.topicId(topicId)
.messageDetails(messageDetails)
.messageType(MessageType.Json)
.build();
client.publishMessage(request);
LOG.infof("Event [%s] published successfully to ONS",
event.eventType());
} catch (Exception e) {
LOG.errorf("Failed to publish event [%s] to ONS: %s",
event.eventType(), e.getMessage());
throw new NotificationPublishException(
event.eventType(), e);
}
}
private NotificationDataPlaneClient createClient()
throws Exception {
BasicAuthenticationDetailsProvider provider;
if (useInstancePrincipal) {
LOG.info("ONS Authentication: Instance Principal (Production)");
provider = InstancePrincipalsAuthenticationDetailsProvider
.builder().build();
} else {
LOG.info("ONS Authentication: Local Config File (Development)");
ConfigFileReader.ConfigFile configFile =
ConfigFileReader.parseDefault();
provider = new ConfigFileAuthenticationDetailsProvider(
configFile);
}
return NotificationDataPlaneClient.builder()
.build(provider);
}
public record OnsMessageBody(String DEFAULT) {
}
public static class NotificationPublishException
extends RuntimeException {
public NotificationPublishException(String eventType,
Throwable cause) {
super("Could not publish event [" + eventType
+ "] to OCI Notifications", cause);
}
}
}
Pontos-chave da implementacao:
NotificationDataPlaneClient: Cliente sync do OCI SDK para o plano de dados do ONS (publicacao). O plano de controle (NotificationControlPlaneClient) seria para criar/topics e subscriptions - nao necessario na aplicacao.MessageType.Json: Indica ao ONS que o body e um JSON estruturado. Isso permite que subscribers recebam o payload parsable.JavaTimeModule: Registra suporte ajava.time.InstantnoObjectMapperstandalone. O mapper CDI do Quarkus ja tem isso, mas nossoObjectMappere criado manualmente para o servico de notificacao.OnsMessageBody: Record que encapsula o campoDEFAULTexigido pela API do ONS. Quando usamosMessageType.Json, o body deve conter um campo chamadoDEFAULTcom o conteudo da mensagem. Este e um requisito da API, nao uma escolha nossa. O nome do campo e literalmenteDEFAULTem maiusculas.NotificationPublishException: Excecao tipada em vez deRuntimeExceptiongenerica. Permite ao chamador distinguir falhas de publicacao de outras excecoes de infraestrutura.- Try-with-resources: O client e criado e fechado a cada publicacao. Em cenarios de alto volume, seria melhor manter um pool ou singleton - mas para o nosso caso (eventos de criacao de artigo), o custo e insignificante.
4. O Service Atualizado
No ArticleService, integrando a chamada tipada ao OciNotificationService:
@Inject
OciNotificationService notificationService;
@Transactional
public ArticleResponse createArticle(CreateArticleRequest request) {
Article article = new Article();
article.title = request.title();
article.author = request.author();
article.content = request.content();
article.createdAt = Instant.now();
repository.persist(article);
ArticleEvent articleEvent = new ArticleEvent(
article.id, article.title, article.author, article.createdAt);
notificationService.publishEvent(
NotificationEvent.of("article.created", articleEvent));
return mapToResponse(article);
}
Note o uso de records tipados: ArticleEvent define o contrato do payload, e NotificationEvent.of() e uma factory method que encapsula o timestamp. O serializador Jackson converte automaticamente os records em JSON. Nenhum Map<String, Object> sem tipagem, nenhum cast inseguro.
5. Configuracao (application.properties)
# OCI Notifications (ONS) Configuration
oci.notification.topic-id=${OCI_NOTIFICATION_TOPIC_ID}
E no .env:
OCI_NOTIFICATION_TOPIC_ID=ocid1.onstopic.oc1.sa-saopaulo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Sem credenciais, sem auth tokens, sem senhas. O ONS usa a mesma autenticacao OCI que o Vault.
6. ConfigSource Convention-Based (BONUS)
Aproveitamos esta phase para refatorar o OciVaultConfigSource para ser baseado em convencao. Em vez de mapear propriedades hardcoded, agora qualquer propriedade X com uma variavel de ambiente X_SECRET_OCID e automaticamente resolvida do Vault:
# Exemplo: resolvendo DB_PASSWORD do Vault
DB_PASSWORD_SECRET_OCID=ocid1.vaultsecret.oc1.xxx...
# Resultado: DB_PASSWORD fica disponivel no MicroProfile Config (ordinal 500)
Isso elimina a necessidade de alterar codigo Java cada vez que um novo segredo precisa ser injetado. A convencao e auto-descritiva e segue o principio de convention over configuration.
O Elo de Ligacao
O diferencial desta implementacao esta na convergencia de autenticacao. O ONS compartilha o mesmo provedor de identidade que ja configuramos para o OCI Vault:
- Dev local:
~/.oci/config(API Key) - Producao: Instance Principals (sem credenciais no codigo)
Isso significa que a mesma identidade OCI que recupera segredos do Vault tambem publica eventos no ONS. Zero novas credenciais para gerenciar. Zero novos segredos para armazenar no Vault. E exatamente o principio do Zero Trust aplicado a mensageria.
A classe OciNotificationService cria seu proprio NotificationDataPlaneClient com o mesmo provedor de autenticacao do OciVaultService. Nao ha dependencia CDI entre eles - cada um tem seu ciclo de vida independente, o que e correto para servicos de infraestrutura que devem funcionar mesmo se o container CDI estiver em estado parcial.
O Campo DEFAULT: Uma Armadilha da API ONS
Durante o E2E test, descobrimos que a API do ONS retorna um erro 400 quando usamos MessageType.Json:
Json formatted body must contain a 'DEFAULT' field
O body da mensagem com MessageType.Json nao e um JSON qualquer: ele precisa seguir o formato de template do ONS, onde cada campo representa um protocolo de entrega. O campo DEFAULT e o fallback para todos os protocolos que nao tem um campo especifico.
A solucao foi encapsular o evento dentro de um record com o campo DEFAULT:
public record OnsMessageBody(String DEFAULT) {
}
Isso garante que o JSON serializado tenha a estrutura esperada pela API: {"DEFAULT": "<evento_json>"}. Subscribers que usam protocolo HTTPS recebem o conteudo do campo DEFAULT, enquanto subscribers email recebem o title como assunto e o DEFAULT como corpo.
Esta e uma daquelas peculiaridades de API que so descobrimos na pratica. Documentacao oficial: PublishMessage API.
OCI Notifications: Sem Custo, Sem Complexidade
O OCI Notifications e genuinamente Always Free, com limites generosos (1M notificacoes HTTPS/mes, 1000 emails/mes). Para a maioria das aplicacoes que precisam de pub/sub simples (notificacoes, webhooks, desacoplamento de servicos), o ONS e a escolha arquitetural correta para o Free Tier.
A licao aqui: antes de implementar uma feature, verifique os service limits do Free Tier. A arquitetura deve ser moldada pelos recursos disponiveis, nao o contrario.
Estrutura do Evento Publicado
Quando um artigo e criado, o ONS recebe esta mensagem JSON:
{
"DEFAULT": {
"eventType": "article.created",
"data": {
"id": 42,
"title": "Cloud Native Java",
"author": "Matheus",
"createdAt": "2026-06-07T17:00:00Z"
},
"timestamp": "2026-06-07T17:00:00.123Z"
}
}
Qualquer subscription (email, HTTPS webhook, Oracle Function) recebe o conteudo do campo DEFAULT e pode reagir de forma independente.
Conclusao
Nossa aplicacao agora e verdadeiramente reativa e desacoplada - e 100% no Free Tier. O backend principal faz o que faz de melhor (gerenciar dados no banco convergente) e delega o restante do ecossistema para reagir aos eventos publicados no OCI Notifications. Sem custo, sem credenciais extras.
No proximo artigo, vamos explorar subscriptions de email no ONS, completando o ciclo publish-subscribe com entrega real de notificacoes.
