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: