skills/dotnet/patterns/outbox/SKILL.md
Ensure reliable event publishing and message delivery by storing events in the same transaction as domain changes, solving the dual-write problem. Use when: - Implementing reliable event publishing in distributed systems - Ensuring events are published even if application crashes - Solving dual-write problem (domain changes + event publishing) - Building event-driven architectures with guaranteed delivery - Implementing eventual consistency between services Triggers: "outbox", "reliable event", "dual-write", "event publishing", "message delivery", "transactional outbox"
npx skillsauth add yeeehaooo/WorkSpace outboxInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ensure reliable event publishing and message delivery by storing events in the same transaction as domain changes.
Applies to:
IOutbox interface defined in Application layer// Application Layer
public interface IOutbox
{
Task AddAsync<T>(T domainEvent) where T : IDomainEvent;
Task<IEnumerable<OutboxMessage>> GetUnpublishedAsync(int batchSize);
Task MarkAsPublishedAsync(Guid messageId);
}
public class OutboxMessage
{
public Guid Id { get; set; }
public string EventType { get; set; }
public string EventData { get; set; }
public DateTime OccurredOn { get; set; }
public DateTime? PublishedOn { get; set; }
}
// Infrastructure Layer
public class DatabaseOutbox : IOutbox
{
private readonly IDbSession _session;
public async Task AddAsync<T>(T domainEvent) where T : IDomainEvent
{
var message = new OutboxMessage
{
Id = Guid.NewGuid(),
EventType = typeof(T).Name,
EventData = JsonSerializer.Serialize(domainEvent),
OccurredOn = DateTime.UtcNow
};
// Store in same transaction as domain changes
await _session.ExecuteAsync(
"INSERT INTO OutboxMessages (Id, EventType, EventData, OccurredOn) VALUES (@Id, @EventType, @EventData, @OccurredOn)",
message
);
}
public async Task<IEnumerable<OutboxMessage>> GetUnpublishedAsync(int batchSize)
{
return await _session.QueryAsync<OutboxMessage>(
"SELECT * FROM OutboxMessages WHERE PublishedOn IS NULL ORDER BY OccurredOn LIMIT @BatchSize",
new { BatchSize = batchSize }
);
}
public async Task MarkAsPublishedAsync(Guid messageId)
{
await _session.ExecuteAsync(
"UPDATE OutboxMessages SET PublishedOn = @PublishedOn WHERE Id = @Id",
new { Id = messageId, PublishedOn = DateTime.UtcNow }
);
}
}
// Background Worker
public class OutboxPublisher : BackgroundService
{
private readonly IOutbox _outbox;
private readonly IDomainEventDispatcher _eventDispatcher;
private readonly ILogger<OutboxPublisher> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var messages = await _outbox.GetUnpublishedAsync(batchSize: 100);
foreach (var message in messages)
{
try
{
var domainEvent = DeserializeEvent(message);
await _eventDispatcher.DispatchAsync(domainEvent, stoppingToken);
await _outbox.MarkAsPublishedAsync(message.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to publish event {MessageId}", message.Id);
// Retry with exponential backoff
}
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in outbox publisher");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
}
// Usage in Command Handler
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand>
{
private readonly IOrderRepository _orderRepo;
private readonly IOutbox _outbox;
private readonly IUnitOfWork _uow;
public async Task<Result> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
await _uow.BeginAsync(ct);
try
{
var order = new Order();
order.Create(cmd.CustomerId, cmd.TotalAmount);
await _orderRepo.AddAsync(order, ct);
// Store event in outbox (same transaction)
foreach (var domainEvent in order.DomainEvents)
{
await _outbox.AddAsync(domainEvent);
}
await _uow.CommitAsync(ct);
order.ClearDomainEvents();
return Result.Success();
}
catch
{
await _uow.RollbackAsync(ct);
throw;
}
}
}
This pattern works well with:
development
Create reusable .NET atomic capability code snippets that can be directly copied and pasted. Use when: - Creating single-purpose code snippets - Building reusable code templates - Implementing atomic technical capabilities - Creating copy-pasteable code blocks - Building snippet library for common patterns Triggers: "create snippet", "code snippet", "reusable snippet", "atomic snippet", "copy-paste code"
development
Create Docker Compose configuration for containerized .NET application development and deployment. Use when: - Containerizing .NET applications - Setting up local development environment with dependencies - Creating multi-container setups (API + DB + Redis) - Defining service dependencies and networking - Building docker-compose.yml for development or production Triggers: "docker compose", "containerize", "multi-container", "docker-compose.yml", "docker setup"
tools
Create adapter structure for integrating third-party APIs in Clean Architecture applications. Use when: - Integrating external APIs or services - Creating HTTP client adapters for third-party services - Implementing API integration with error handling - Setting up adapter pattern for external dependencies - Building resilient external service integrations Triggers: "api adapter", "third-party api", "external service", "http client adapter", "api integration"
development
Enterprise backend structure built on Clean Architecture, DDD, CQRS, and Vertical Slice API Design with Dapper-first persistence. Use when: - Creating new enterprise backend projects - Implementing Clean Architecture with DDD and CQRS - Building vertical slice API endpoints - Using Dapper as primary persistence mechanism - Organizing modules by UseCase-driven and Model-driven separation Triggers: "dmis structure", "clean architecture", "enterprise backend", "DDD CQRS", "vertical slice", "dapper"