Skip to content

Best Practices

A. Shafie edited this page Sep 26, 2025 · 1 revision

Best Practices

This guide provides a consolidated list of best practices and conventions to follow when building applications with LiteBus. Adhering to these principles will help you create systems that are maintainable, scalable, and easy to reason about.

1. Message Design

  • Immutability: Design your command, query, and event objects to be immutable. Use C# records or classes with init-only properties. This prevents state from being accidentally modified during processing.
    // Good: Immutable record
    public sealed record CreateProductCommand(string Name, decimal Price) : ICommand<Guid>;
  • Clarity and Focus: Each message should represent a single, well-defined action or piece of information. Avoid creating generic "god" messages that do too many things.
  • Naming Conventions:
    • Commands: Use imperative verbs in the present tense (e.g., CreateUser, UpdateAddress).
    • Queries: Use descriptive nouns (e.g., GetProductById, FindUsersByRole).
    • Events: Use verbs in the past tense (e.g., OrderShipped, UserRegistered).
  • Return DTOs, Not Domain Entities: Query handlers should return Data Transfer Objects (DTOs) or view models, not your internal domain entities. This creates a clean public contract for your application layer and prevents leaking domain logic.

2. Handler Design

  • Single Responsibility: A handler should do one thing well. A command handler should execute its business logic; a pre-handler should validate; a post-handler should handle side effects like notifications.
  • Idempotency: For critical operations (especially with the Command Inbox or in distributed systems), design your handlers to be idempotent. This means they can be safely executed multiple times with the same input without causing incorrect results.
  • Dependency Injection: Handlers should be stateless. All dependencies (repositories, services, etc.) should be injected via the constructor.
  • Avoid Chaining: Avoid having one handler call the mediator to send another message. This creates "magic" action-at-a-distance and makes the control flow very difficult to follow. For complex workflows, use a Saga or Process Manager pattern instead.

3. Pipeline Management

  • Use Pre-Handlers for Guard Clauses: Pre-handlers are the ideal place for validation, permission checks, and other "guard clauses" that should run before any business logic.
  • Use Post-Handlers for Side Effects: Post-handlers are perfect for operations that should occur after the main transaction has completed, such as publishing integration events, sending notifications, or clearing caches.
  • Centralize Cross-Cutting Concerns: Use polymorphic dispatch on base interfaces (e.g., IAuditableCommand) to implement cross-cutting concerns like auditing and authorization in a single place.

4. Error Handling

  • Throw Specific Exceptions: In your handlers, throw specific, custom exceptions (e.g., ProductNotFoundException) instead of generic ones.
  • Use Error Handlers for Cross-Cutting Error Logic: Implement ICommandErrorHandler (or query/event equivalents) to handle logging, metrics, or transforming exceptions into user-friendly error responses in a centralized way.

5. Configuration and Testing

  • Register Handlers at Startup: Configure all your LiteBus modules and register your handlers during application startup for optimal performance.
  • Isolate Tests: Use a library like LiteBus.Testing or manually clear the MessageRegistry between integration tests to ensure they are isolated.
  • Unit Test Logic, Integration Test Pipelines: Write fine-grained unit tests for the business logic inside your handlers. Write a smaller number of integration tests to verify that your most important pipelines (including pre/post handlers) are wired correctly.
Clone this wiki locally