Skip to main content

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:

  1. Start with a question about your use case
  2. Follow the arrows based on your answers
  3. Arrive at a recommendation with reasoning
  4. See examples of the recommended pattern

๐Ÿ—๏ธ Aggregate Design Decisionsโ€‹

When to Create a New Aggregate?โ€‹

Decision Factors:

FactorNew AggregateEntity within Aggregate
IdentityGlobally unique business identityIdentity dependent on parent
LifecycleIndependent creation/deletionCreated/deleted with parent
InvariantsEnforces own business rulesParticipates in parent's rules
ConsistencyOwn transaction boundaryPart of parent's transaction
SizeReasonable number of eventsDoesn'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โ€‹

DecisionSmall ScaleMedium ScaleLarge Scale
Event StoreEAF PostgreSQLEAF PostgreSQLConsider Axon Server
ProcessorsTracking (small batches)Tracking (medium batches)Tracking (large batches)
TenancyRow-levelRow-levelSchema-level
AggregatesSimple boundariesClear boundariesVery clear boundaries
TestingUnit + IntegrationUnit + Integration + E2EAll levels + Performance
MonitoringBasic metricsComprehensive metricsFull observability

๐Ÿ’ก Remember: These are guidelines, not rigid rules. Always consider your specific context and requirements when making architectural decisions!