Cafe Application
A complete example demonstrating Nimbus messaging patterns in a real-world scenario
The Cafe Application is a comprehensive example that demonstrates how to build a distributed system using Nimbus. It simulates a coffee shop where customers place orders, which are then processed through multiple services working together via message-based communication.
Overview
The example models a coffee shop workflow with three main actors:
- Cashier - Takes customer orders and processes payments
- Barista - Makes the coffee
- Waiter - Coordinates delivery (only delivers when both paid and made)
This demonstrates how Nimbus enables loosely-coupled, event-driven architectures where services can operate independently while maintaining business logic consistency.
Architecture
Projects
The example consists of four .NET projects:
Cafe.Messages (netstandard2.0)
- Shared message contracts library
- Contains commands and events used across all services
- No business logic, just data contracts
Cafe.Cashier (console app)
- Handles
PlaceOrderCommand - Publishes
OrderPaidForEventandOrderPlacedEvent - Includes a
CustomerOrderGeneratorthat simulates customers placing orders every 5 seconds
Cafe.Barista (console app)
- Subscribes to
OrderPlacedEventas a competing consumer - Simulates coffee preparation (1 second delay)
- Publishes
OrderIsReadyEventwhen done
Cafe.Waiter (console app)
- Subscribes to both
OrderPaidForEventandOrderIsReadyEventas a competing consumer - Maintains in-memory state of which orders are paid/made
- Only delivers orders when both conditions are met (demonstrating event correlation)
Message Flow
Customer (simulated)
|
v
[PlaceOrderCommand] ---> Cashier
|
|---> publishes OrderPaidForEvent
|---> publishes OrderPlacedEvent
|
v
Barista (handles OrderPlacedEvent)
|
|---> simulates making coffee (1 sec)
|---> publishes OrderIsReadyEvent
|
v
Waiter
(correlates events)
|
v
Delivers when both paid AND ready
Key Nimbus Concepts Demonstrated
1. Commands vs Events
Commands (fire-and-forget, single handler):
public class PlaceOrderCommand : IBusCommand
{
public Guid OrderId { get; set; }
public string CustomerName { get; set; }
public string CoffeeType { get; set; }
}
Events (publish-subscribe, multiple handlers):
public class OrderPlacedEvent : IBusEvent
{
public Guid OrderId { get; set; }
public string CoffeeType { get; set; }
public string CustomerName { get; set; }
}
2. Competing Consumers
The Barista uses IHandleCompetingEvent<T> which allows multiple Barista instances to share the workload:
public class MakeThemTheirCoffee : IHandleCompetingEvent<OrderPlacedEvent>
{
private readonly IBus _bus;
public async Task Handle(OrderPlacedEvent busEvent)
{
// Simulate making coffee
await Task.Delay(TimeSpan.FromSeconds(1));
// Publish completion event
await _bus.Publish(new OrderIsReadyEvent(
busEvent.OrderId,
busEvent.CoffeeType,
busEvent.CustomerName));
}
}
This pattern enables horizontal scaling - you can run multiple Barista instances and they’ll automatically share orders.
3. Event Correlation
The Waiter demonstrates how to correlate multiple events using a shared correlation ID (the OrderId):
public class OrderDeliveryService : IOrderDeliveryService
{
private readonly List<Guid> _madeOrderIds = new List<Guid>();
private readonly List<Guid> _paidOrderIds = new List<Guid>();
public void MarkAsPaid(Guid orderId)
{
_paidOrderIds.Add(orderId);
CheckWhetherWeShouldDeliver(orderId);
}
public void MarkAsMade(Guid orderId)
{
_madeOrderIds.Add(orderId);
CheckWhetherWeShouldDeliver(orderId);
}
private void CheckWhetherWeShouldDeliver(Guid orderId)
{
if (!_madeOrderIds.Contains(orderId))
{
_logger.Information("{OrderId} isn't ready yet.", orderId);
return;
}
if (!_paidOrderIds.Contains(orderId))
{
_logger.Information("{OrderId} hasn't been paid for yet.", orderId);
return;
}
DeliverOrderToCustomer(orderId);
}
}
This pattern handles the distributed saga where order completion depends on multiple independent operations.
4. Autofac Integration
Each service uses Autofac modules to configure Nimbus:
var builder = new ContainerBuilder();
builder.RegisterAssemblyModules(typeof(Program).Assembly);
using (builder.Build())
{
Console.ReadKey();
return;
}
The BusModule shows how to configure Nimbus with different transports:
builder.RegisterNimbus(handlerTypesProvider);
builder.Register(componentContext => new BusBuilder()
.Configure()
.WithTransport(new AMQPTransportConfiguration()
.WithBrokerUri("amqp://localhost:5672")
.WithCredentials("admin", "admin"))
.WithNames("Cashier", Environment.MachineName)
.WithTypesFrom(handlerTypesProvider)
.WithAutofacDefaults(componentContext)
.WithSerilogLogger()
.WithJsonSerializer()
.Build())
.As<IBus>()
.AutoActivate()
.OnActivated(c => c.Instance.Start())
.SingleInstance();
5. Transport Agnostic
The BusModule includes commented examples for switching between transports:
- Azure Service Bus (with large message support via Blob Storage)
- Redis
- AMQP (ActiveMQ Artemis) (currently active in the example)
You can switch transports by simply changing the configuration - no changes to business logic required.
6. Structured Logging
All services use Serilog with Seq integration for distributed tracing:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.MinimumLevel.Debug()
.WriteTo.Seq("http://localhost:5341")
.Enrich.WithProperty("Application", "Cashier")
.CreateLogger();
This allows you to trace orders across all services in Seq’s UI.
Running the Example
Prerequisites
-
Message Broker - Choose one:
- ActiveMQ:
docker run -p 5672:5672 apache/activemq-artemis - Redis:
docker run -p 6379:6379 redis - Azure Service Bus: Set environment variables for connection strings
- ActiveMQ:
-
Seq (optional, for log aggregation):
docker run -d -p 5341:80 -e ACCEPT_EULA=Y datalust/seq
Running the Services
- Start your chosen message broker
- Start all three services in separate terminal windows:
# Terminal 1
cd src/Cafe.Cashier
dotnet run
# Terminal 2
cd src/Cafe.Barista
dotnet run
# Terminal 3
cd src/Cafe.Waiter
dotnet run
What to Observe
Once running, you’ll see:
- Cashier generates random orders every 5 seconds
- Barista picks up orders and “makes” them (1 second delay)
- Waiter coordinates delivery, logging when orders aren’t ready yet
- Orders are successfully delivered once both paid and made
Try scaling by running multiple Barista instances - they’ll automatically share the workload via competing consumers.
Learning Points
This example teaches:
- Command/Event Distinction - When to use commands vs events
- Competing Consumers - How to scale message handlers horizontally
- Event Correlation - Coordinating distributed workflows with correlation IDs
- Transport Independence - Swapping message brokers without code changes
- Autofac Integration - Dependency injection with Nimbus
- Structured Logging - Distributed tracing across services
Source Code
The complete source code is available in the Nimbus GitHub repository under the Cafe.* projects.