Skip to main content

Topic Naming and Patterns Guide

Comprehensive guide to designing effective topic hierarchies, naming conventions, and wildcard patterns for NatsPubsub applications.

Table of Contents


Overview

Topics (also called subjects in NATS) are the foundation of message routing in NatsPubsub. Well-designed topics make your system:

  • Flexible: Easy to add new consumers
  • Scalable: Efficient message routing
  • Maintainable: Clear organization and intent
  • Observable: Easy to monitor and debug

Topic Basics

In NatsPubsub, topics are automatically prefixed with environment and app name:

{env}.{appName}.{topic}

Example:

// You publish to:
NatsPubsub.publish("order.created", orderData);

// NATS receives:
("production.order-service.order.created");

Topic Structure

Use dot notation with hierarchical levels:

resource.action
domain.resource.action
service.domain.resource.action

Examples by Complexity

Simple (2 levels)

order.created
order.updated
order.cancelled

user.registered
user.updated
user.deleted

Medium (3 levels)

ecommerce.order.created
ecommerce.order.shipped
ecommerce.payment.completed

auth.user.registered
auth.user.verified
auth.session.created

Complex (4+ levels)

platform.ecommerce.order.payment.completed
platform.ecommerce.inventory.item.reserved
platform.notification.email.sent
platform.notification.sms.delivered

Level Guidelines

LevelsUse CaseExample
2Single service, simple domainorder.created
3Multiple services, clear domainsecommerce.order.created
4+Large microservices architectureplatform.ecommerce.order.created

Naming Conventions

General Rules

  1. Use lowercase: order.created not Order.Created
  2. Use dots for hierarchy: order.payment.completed
  3. Use past tense for events: created, updated, completed
  4. Use present tense for commands: create, update, process
  5. Be consistent: Pick a convention and stick to it

Resource Naming

// Good: Singular nouns
"order.created";
"user.registered";
"payment.completed";

// Bad: Plural or mixed
"orders.created";
"user.registration";
"payments.complete";

Action Naming

// Good: Clear, consistent actions
"order.created"; // New order
"order.updated"; // Order modified
"order.cancelled"; // Order cancelled
"order.shipped"; // Order shipped
"order.delivered"; // Order delivered

// Bad: Unclear or inconsistent
"order.new";
"order.change";
"order.cancel";
"order.ship";

Context-Specific Naming

// E-commerce
"order.created";
"order.payment.authorized";
"order.payment.captured";
"order.fulfillment.started";
"order.fulfillment.completed";

// Authentication
"user.registered";
"user.email.verified";
"user.password.reset";
"session.created";
"session.expired";

// Notifications
"notification.email.queued";
"notification.email.sent";
"notification.email.delivered";
"notification.email.bounced";

Hierarchical Patterns

By Domain

// Organize by business domain
ecommerce.order.created
ecommerce.product.updated
ecommerce.inventory.reserved

payments.transaction.completed
payments.refund.processed
payments.dispute.opened

notifications.email.sent
notifications.sms.sent
notifications.push.sent

By Service

// Organize by service ownership
order-service.order.created
order-service.order.updated

inventory-service.stock.reserved
inventory-service.stock.released

notification-service.email.sent
notification-service.sms.sent

By Event Type

// Organize by event characteristics
events.lifecycle.order.created
events.lifecycle.order.completed

events.integration.payment.received
events.integration.shipment.tracking

events.analytics.user.action
events.analytics.conversion.completed

Wildcard Usage

NATS supports two wildcards:

  • * - Matches exactly one token
  • > - Matches one or more tokens

Single-Level Wildcard (*)

// Subscribe to all order events
"production.order-service.order.*";

// Matches:
// - order.created
// - order.updated
// - order.cancelled

// Does NOT match:
// - order.payment.completed (too many levels)
// - user.created (different resource)

JavaScript Example

class OrderEventsSubscriber extends Subscriber {
constructor() {
super("production.order-service.order.*"); // Wildcard
}

async handle(message: any, metadata: TopicMetadata) {
const action = metadata.topic.split(".").pop();

switch (action) {
case "created":
await this.handleOrderCreated(message);
break;
case "updated":
await this.handleOrderUpdated(message);
break;
case "cancelled":
await this.handleOrderCancelled(message);
break;
}
}
}

Ruby Example

class OrderEventsSubscriber < NatsPubsub::Subscriber
subscribe_to 'order.*'

def handle(message, context)
action = context.topic.split('.').last

case action
when 'created'
handle_order_created(message)
when 'updated'
handle_order_updated(message)
when 'cancelled'
handle_order_cancelled(message)
end
end
end

Multi-Level Wildcard (>)

// Subscribe to ALL events from order-service
"production.order-service.>";

// Matches:
// - order.created
// - order.updated
// - order.payment.completed
// - order.fulfillment.shipped
// - user.created (if published by order-service)

Use Cases

// 1. Audit logging (capture everything)
class AuditSubscriber extends Subscriber {
constructor() {
super("production.*.>"); // All events from all services
}

async handle(message: any, metadata: TopicMetadata) {
await AuditLog.create({
topic: metadata.topic,
data: message,
timestamp: new Date(),
});
}
}

// 2. Analytics (specific domain, all events)
class EcommerceAnalyticsSubscriber extends Subscriber {
constructor() {
super("production.order-service.ecommerce.>");
}

async handle(message: any, metadata: TopicMetadata) {
await Analytics.track(metadata.topic, message);
}
}

// 3. Cross-service monitoring
class MonitoringSubscriber extends Subscriber {
constructor() {
super("production.*.order.>"); // All order events across services
}

async handle(message: any, metadata: TopicMetadata) {
await updateMetrics(metadata.topic, message);
}
}

Wildcard Best Practices

// Good: Specific wildcards
"order.*"; // Only order actions
"ecommerce.*.created"; // Only created events in ecommerce
"notification.email.>"; // All email notification events

// Bad: Too broad
"*"; // Matches everything (don't do this)
"*.>"; // Also matches everything
"production.*.*.>"; // Too generic, hard to debug

Versioning Strategies

Strategy 1: Version in Topic Name

// V1
"order.v1.created";
"order.v1.updated";

// V2 (breaking changes)
"order.v2.created";
"order.v2.updated";

// Subscribers choose version
class OrderV1Subscriber extends Subscriber {
constructor() {
super("production.order-service.order.v1.*");
}
}

class OrderV2Subscriber extends Subscriber {
constructor() {
super("production.order-service.order.v2.*");
}
}

Strategy 2: Version in Metadata

// Publishing
await NatsPubsub.publish("order.created", orderData, {
version: "2.0",
schema_version: "v2",
});

// Subscribing
class OrderSubscriber extends Subscriber {
async handle(message: any, metadata: TopicMetadata) {
const version = metadata.version || "1.0";

if (version === "1.0") {
await this.handleV1(message);
} else if (version === "2.0") {
await this.handleV2(message);
}
}
}

Strategy 3: Separate Streams

// V1 stream: order-events-v1
NatsPubsub.publish("v1.order.created", orderData);

// V2 stream: order-events-v2
NatsPubsub.publish("v2.order.created", orderData);

// Consumers subscribe to appropriate stream

Recommendation

For most cases, use Strategy 2 (metadata) because:

  • Clean topic names
  • Flexible version handling
  • Easy migration path
  • No topic explosion

Best Practices

1. Keep Topics Short but Descriptive

// Good
"order.created";
"payment.completed";
"user.registered";

// Bad
"order-created-in-system";
"payment-processing-completed-successfully";
"new-user-registration-event";

2. Use Consistent Verb Tenses

// Good: All past tense
"order.created";
"order.updated";
"order.shipped";

// Bad: Mixed tenses
"order.create";
"order.updated";
"order.shipping";

3. Avoid Deep Nesting

// Good: 3-4 levels max
"ecommerce.order.payment.completed";

// Bad: Too deep
"platform.region.service.domain.resource.action.status";

4. Make Topics Self-Documenting

// Good: Clear intent
"order.payment.authorized";
"order.payment.captured";
"order.payment.failed";

// Bad: Unclear
"order.payment.state1";
"order.payment.state2";
"order.payment.state3";

5. Plan for Wildcards

// Good: Wildcard-friendly structure
"notification.email.sent";
"notification.email.delivered";
"notification.email.bounced";
// Subscribe with: 'notification.email.*'

// Bad: Inconsistent structure
"notification.sent.email";
"email.delivered";
"notification.bounced";
// Can't use wildcards effectively

Anti-Patterns

1. Using Dots in Action Names

// Bad
"order.status.updated"; // Looks like 3 levels but is 2

// Good
"order.status_updated"; // Clear 2 levels
"order.status-updated"; // Clear 2 levels

2. Mixing Domains

// Bad: Mixed concerns in one topic
"order-and-payment.completed";

// Good: Separate topics
"order.completed";
"payment.completed";

3. Using Dynamic Values in Topics

// Bad: Topic includes data
await NatsPubsub.publish(`order.${orderId}.created`, data);

// Good: Data in message body
await NatsPubsub.publish("order.created", {
order_id: orderId,
...data,
});

4. Too Many Levels

// Bad: 6+ levels
"platform.region.us-east.service.ecommerce.order.payment.credit-card.completed";

// Good: 4 levels max
"ecommerce.order.payment.completed";

5. Inconsistent Naming

// Bad: Inconsistent across service
"order.created";
"order-updated";
"orderCancelled";

// Good: Consistent
"order.created";
"order.updated";
"order.cancelled";

Examples

E-Commerce System

// Orders
"order.created";
"order.updated";
"order.cancelled";
"order.payment.pending";
"order.payment.completed";
"order.payment.failed";
"order.fulfillment.picking";
"order.fulfillment.packed";
"order.fulfillment.shipped";
"order.delivered";

// Products
"product.created";
"product.updated";
"product.deleted";
"product.price.changed";
"product.inventory.low";
"product.inventory.out";

// Customers
"customer.registered";
"customer.updated";
"customer.order.placed";
"customer.subscription.created";
"customer.subscription.cancelled";

SaaS Platform

// Users
"user.registered";
"user.email.verified";
"user.profile.updated";
"user.subscription.upgraded";
"user.subscription.downgraded";
"user.subscription.cancelled";

// Organizations
"org.created";
"org.member.added";
"org.member.removed";
"org.billing.payment.succeeded";
"org.billing.payment.failed";
"org.usage.limit.reached";

// Features
"feature.flag.updated";
"feature.access.granted";
"feature.access.revoked";

Multi-Service Architecture

// Service A: Order Service
"order-service.order.created";
"order-service.order.updated";

// Service B: Payment Service
"payment-service.payment.authorized";
"payment-service.payment.captured";

// Service C: Notification Service
"notification-service.email.sent";
"notification-service.sms.sent";

// Cross-service subscriber
class OrderPaymentSyncSubscriber extends Subscriber {
constructor() {
// Listen to payment events
super("production.payment-service.payment.*");
}

async handle(message: any, metadata: TopicMetadata) {
// Update order based on payment events
await syncOrderPaymentStatus(message);
}
}


Navigation: