Skip to main content

Dynamic Topics

Dynamic topics allow you to determine the topic name at runtime based on message content. This is useful for multi-tenant scenarios where each tenant needs isolated message streams.

When to Use Dynamic Topics

Use dynamic topics when:

  • Building multi-tenant applications with tenant isolation
  • Routing messages based on content (e.g., region, environment)
  • Need for logical separation of message streams
  • Partitioning messages across multiple topics

Configuration

Use WithTopicName with a function that takes the message and returns the topic name:

services.AddJustSaying(config =>
{
config.Messaging(x => x.WithRegion("us-east-1"));

config.Publications(x =>
{
x.WithTopic<OrderPlacedEvent>(cfg =>
{
cfg.WithTopicName(msg =>
{
var order = (OrderPlacedEvent)msg;
return $"tenant-{order.TenantId}-orders";
});
});
});
});

Multi-Tenant Example

Message Definition

Define messages with tenant information:

public class OrderPlacedEvent : Message
{
public string TenantId { get; set; }
public int OrderId { get; set; }
public string Description { get; set; }
}

Publisher Configuration

Configure dynamic topic naming based on tenant:

config.Publications(x =>
{
x.WithTopic<OrderPlacedEvent>(cfg =>
{
cfg.WithTopicName(msg =>
{
var order = (OrderPlacedEvent)msg;
return $"tenant-{order.TenantId}-orders";
});
});
});

Publishing Messages

Publish messages normally - the topic is determined automatically:

// Publishes to topic: tenant-acme-orders
await publisher.PublishAsync(new OrderPlacedEvent
{
TenantId = "acme",
OrderId = 123,
Description = "Office supplies"
});

// Publishes to topic: tenant-contoso-orders
await publisher.PublishAsync(new OrderPlacedEvent
{
TenantId = "contoso",
OrderId = 456,
Description = "Electronics"
});

Subscriber Configuration

Subscribers can use dynamic topic names as well:

config.Subscriptions(x =>
{
// Subscribe to specific tenant topics
x.ForTopic<OrderPlacedEvent>("tenant-acme-orders");
x.ForTopic<OrderPlacedEvent>("tenant-contoso-orders");
});

services.AddJustSayingHandler<OrderPlacedEvent, OrderPlacedEventHandler>();

For dynamic subscription scenarios, you may need to create separate bus instances per tenant.

Performance Considerations

Topic Creation Overhead

Each unique topic name requires AWS API calls to create the topic:

  • First message to a new topic: ~1-2 seconds for topic creation
  • Subsequent messages: Normal publish latency

Recommendation: Pre-create topics for known tenants using infrastructure-as-code (CloudFormation, Terraform).

Topic Limits

AWS accounts have limits on SNS topics (default: 100,000 topics per region). Monitor topic count in multi-tenant scenarios:

aws sns list-topics --region us-east-1 | jq '.Topics | length'

Caching

JustSaying caches topic ARNs after creation, so topic lookup overhead only occurs once per topic per application instance.

Naming Conventions

Best Practices

  1. Use consistent prefixes: tenant-{id}-{type} makes topics easy to identify
  2. Lowercase names: SNS topic names are case-insensitive but lowercase is convention
  3. Alphanumeric only: Use letters, numbers, hyphens, and underscores
  4. Keep names short: Topic names have a 256-character limit

Examples

Good:

  • tenant-acme-orders
  • region-us-east-payments
  • env-prod-notifications

Avoid:

  • Tenant_ACME_Orders (mixed case, underscores)
  • tenant:acme:orders (colons not recommended)
  • Very long names with excessive detail

Security and Isolation

IAM Policies for Dynamic Topics

Grant permissions using wildcard patterns:

{
"Effect": "Allow",
"Action": [
"sns:CreateTopic",
"sns:Publish"
],
"Resource": "arn:aws:sns:us-east-1:123456789012:tenant-*"
}

Tenant Isolation

Dynamic topics provide logical isolation but all topics share the same AWS account. For stronger isolation:

  • Use separate AWS accounts per tenant
  • Implement application-level access controls
  • Audit topic access using CloudTrail

Alternative Patterns

Message Filtering

Instead of dynamic topics, use SNS subscription filters:

// Single topic with message attributes
await publisher.PublishAsync(message, new PublishMetadata
{
MessageAttributes = new Dictionary<string, MessageAttributeValue>
{
["TenantId"] = new MessageAttributeValue { StringValue = "acme" }
}
});

Subscribers filter using SNS filter policies. This reduces topic count but requires filter configuration.

Queue-Per-Tenant

Use dynamic queue names instead of dynamic topics:

config.Publications(x =>
{
x.WithQueue<OrderCommand>(cfg =>
{
cfg.WithQueueName(msg =>
{
var cmd = (OrderCommand)msg;
return $"tenant-{cmd.TenantId}-commands";
});
});
});

Complete Example

Application Configuration

services.AddJustSaying(config =>
{
config.Messaging(x => x.WithRegion("us-east-1"));

config.Publications(x =>
{
// Dynamic topic based on tenant
x.WithTopic<TenantEvent>(cfg =>
{
cfg.WithTopicName(msg =>
{
var evt = (TenantEvent)msg;

// Validate tenant ID
if (string.IsNullOrEmpty(evt.TenantId))
throw new ArgumentException("TenantId is required");

return $"tenant-{evt.TenantId.ToLowerInvariant()}-events";
});
});
});
});

Publishing Service

public class TenantOrderService
{
private readonly IMessagePublisher _publisher;

public TenantOrderService(IMessagePublisher publisher)
{
_publisher = publisher;
}

public async Task PlaceOrder(string tenantId, int orderId)
{
// Topic determined from TenantId at publish time
await _publisher.PublishAsync(new OrderPlacedEvent
{
TenantId = tenantId,
OrderId = orderId,
Description = "New order"
});
}
}

Troubleshooting

"Topic name is invalid"

Ensure topic names meet AWS requirements:

  • 1-256 characters
  • Alphanumeric, hyphens, and underscores only
  • Cannot start with "aws"

Too many topics created

Monitor and clean up unused topics:

# List all topics
aws sns list-topics --region us-east-1

# Delete unused topics
aws sns delete-topic --topic-arn arn:aws:sns:region:account:topic-name

Performance issues with topic creation

Pre-create topics using infrastructure-as-code to avoid runtime creation overhead.

See Also