Decision Trees & Guidelines
Quick decision trees and guidelines to help you make the right architectural choices when using Axon Framework with EAF.
๐ฏ How to Use This Guideโ
Each decision tree follows this pattern:
- Start with a question about your use case
- Follow the arrows based on your answers
- Arrive at a recommendation with reasoning
- See examples of the recommended pattern
๐๏ธ Aggregate Design Decisionsโ
When to Create a New Aggregate?โ
Decision Factors:
Factor | New Aggregate | Entity within Aggregate |
---|---|---|
Identity | Globally unique business identity | Identity dependent on parent |
Lifecycle | Independent creation/deletion | Created/deleted with parent |
Invariants | Enforces own business rules | Participates in parent's rules |
Consistency | Own transaction boundary | Part of parent's transaction |
Size | Reasonable number of events | Doesn't bloat parent aggregate |
Examples:
// โ
Good: Separate aggregates
class Order { ... } // Independent lifecycle
class Customer { ... } // Independent lifecycle
class Product { ... } // Independent lifecycle
// โ
Good: Entity within aggregate
class Order {
private val lineItems: MutableList<OrderLineItem> = mutableListOf()
// OrderLineItem has no independent lifecycle
}
// โ Avoid: Everything in one aggregate
class OrderAggregate {
// Order, Customer, Product, Payment, Shipping all in one
// This violates aggregate boundaries
}
Aggregate Size Guidelinesโ
๐ Event Store Strategyโ
PostgreSQL vs Axon Server Decisionโ
When to Use EAF PostgreSQL:
- โ Starting with CQRS/ES
- โ Single instance deployment
- โ < 1M events per day
- โ Need multi-tenancy
- โ Want database familiarity
When to Consider Axon Server:
- โ Need horizontal scaling
- โ Multiple service instances
- โ > 1M events per day
- โ Need advanced routing
- โ Plan for distributed architecture
๐ Event Processing Strategyโ
Tracking vs Subscribing Processorsโ
Configuration Examples:
// โ
Real-time projections - Tracking
config.registerTrackingEventProcessor("user-projections") {
TrackingEventProcessorConfiguration
.forParallelProcessing(2)
.andBatchSize(10) // Small batches for real-time
.andInitialTrackingToken { it.eventStore().createHeadToken() }
}
// โ
Analytics - Tracking with large batches
config.registerTrackingEventProcessor("user-analytics") {
TrackingEventProcessorConfiguration
.forSingleThreadedProcessing()
.andBatchSize(100) // Large batches for efficiency
.andInitialTrackingToken { it.eventStore().createHeadToken() }
}
// โ ๏ธ Only when you absolutely cannot use Tracking
config.registerSubscribingEventProcessor("immediate-notifications")
๐ข Multi-Tenancy Patternsโ
Tenant Isolation Strategyโ
EAF Recommendation: Row-Level Tenancy
// โ
EAF Standard Pattern
@Entity
@Table(name = "user_projections")
data class UserProjection(
@Id val userId: String,
@Column(nullable = false) val tenantId: String, // Always include
// ... other fields
)
// Always filter by tenant
interface UserProjectionRepository : JpaRepository<UserProjection, String> {
fun findByTenantIdAndUserId(tenantId: String, userId: String): UserProjection?
fun findAllByTenantId(tenantId: String): List<UserProjection>
}
// Always validate tenant context
@EventHandler
fun on(event: UserCreatedEvent, @MetaData("tenant_id") tenantId: String) {
require(event.tenantId == tenantId) { "Tenant mismatch" }
// ... handle event
}
๐ Saga Usage Decisionsโ
When to Use Sagasโ
Saga Use Cases:
// โ
Good Saga use case - Order fulfillment
@Saga
class OrderFulfillmentSaga {
// Coordinates: Inventory โ Payment โ Shipping โ Notification
// Handles: Timeouts, compensation, external service calls
}
// โ
Good Saga use case - User onboarding
@Saga
class UserOnboardingSaga {
// Coordinates: Account creation โ License allocation โ Welcome email
// Handles: External service integration, compensation
}
// โ Avoid Saga - Simple event propagation
// Just use event handlers instead:
@EventHandler
fun on(event: UserCreatedEvent) {
// Simple notification - no saga needed
notificationService.sendWelcomeEmail(event.userId)
}
๐ Performance Decisionsโ
Optimization Strategyโ
Performance Tuning Checklist:
// โ
Command Optimization
@CommandHandler
fun handle(command: CreateUserCommand) {
// Keep business logic minimal
// Defer heavy operations to event handlers
val user = User(command)
repository.save(user)
}
// โ
Query Optimization
@Entity
@Table(indexes = [
Index(name = "idx_tenant_user", columnList = "tenantId,userId"),
Index(name = "idx_tenant_email", columnList = "tenantId,email")
])
data class UserProjection(...)
// โ
Event Processing Optimization
config.registerTrackingEventProcessor("projections") {
TrackingEventProcessorConfiguration
.forParallelProcessing(4) // Parallel threads
.andBatchSize(50) // Optimize batch size
}
// โ
Caching
@Cacheable(value = ["users"], key = "#userId")
fun findUser(userId: String): UserProjection?
๐งช Testing Strategy Decisionsโ
What to Test and Howโ
๐จ Common Anti-Patterns to Avoidโ
โ What NOT to Doโ
๐ฏ Quick Decision Summaryโ
Decision | Small Scale | Medium Scale | Large Scale |
---|---|---|---|
Event Store | EAF PostgreSQL | EAF PostgreSQL | Consider Axon Server |
Processors | Tracking (small batches) | Tracking (medium batches) | Tracking (large batches) |
Tenancy | Row-level | Row-level | Schema-level |
Aggregates | Simple boundaries | Clear boundaries | Very clear boundaries |
Testing | Unit + Integration | Unit + Integration + E2E | All levels + Performance |
Monitoring | Basic metrics | Comprehensive metrics | Full observability |
๐ก Remember: These are guidelines, not rigid rules. Always consider your specific context and requirements when making architectural decisions!