Skip to content

Optimizing Tree Structure

The structure of your Bonsai trees has a significant impact on performance. This guide provides recommendations for designing efficient tree structures that optimize evaluation performance.

Tree Depth

The depth of a tree is the number of edges that need to be traversed from the root to the deepest leaf. Deep trees can lead to longer evaluation times:

  • Keep trees shallow: Aim for a maximum depth of 5-10 levels
  • Use flatter structures: Consider reorganizing deep hierarchies into flatter structures
  • Balance the tree: Distribute decision points evenly to avoid heavily skewed trees

Edge Count and Order

The number of edges per knot and their order affects how quickly Bonsai can find the right path:

  • Limit edges per knot: Keep the number of edges per knot below 10-20
  • Order edges by likelihood: Put the most likely matches first to reduce the number of evaluations
  • Use default edges wisely: Always include a default edge (with no filters) as the last edge on a knot

Example of ordering edges by likelihood:

// Add the most common case first
bonsai.addVariation(rootKnot.getId(), Variation.builder()
    .knotId(commonCaseKnot.getId())
    .filters(List.of(
        Filter.builder()
            .path("$.user.type")
            .operator(Operator.EQUALS)
            .value("regular")
            .build()
    ))
    .build());

// Add less common cases next
bonsai.addVariation(rootKnot.getId(), Variation.builder()
    .knotId(premiumCaseKnot.getId())
    .filters(List.of(
        Filter.builder()
            .path("$.user.type")
            .operator(Operator.EQUALS)
            .value("premium")
            .build()
    ))
    .build());

// Add the default case last
bonsai.addVariation(rootKnot.getId(), Variation.builder()
    .knotId(defaultCaseKnot.getId())
    .filters(List.of())
    .build());

Filter Complexity

The complexity of filter conditions affects evaluation time:

  • Use simple filters: Keep filter conditions simple and focused
  • Limit the number of filters per edge: Keep the number of filters per edge below 5-10
  • Avoid complex JsonPath expressions: Complex expressions can be expensive to evaluate
  • Use efficient operators: Some operators are more efficient than others

Example of simplifying filters:

// Instead of this complex filter
bonsai.addVariation(rootKnot.getId(), Variation.builder()
    .knotId(targetKnot.getId())
    .filters(List.of(
        Filter.builder()
            .path("$.user.preferences.settings.theme.color")
            .operator(Operator.EQUALS)
            .value("dark")
            .build()
    ))
    .build());

// Consider restructuring your context data
// And using a simpler filter
bonsai.addVariation(rootKnot.getId(), Variation.builder()
    .knotId(targetKnot.getId())
    .filters(List.of(
        Filter.builder()
            .path("$.user.theme")
            .operator(Operator.EQUALS)
            .value("dark")
            .build()
    ))
    .build());

Tree Structure Patterns

Certain tree structure patterns can lead to better performance:

Decision Trees

For simple decision logic, use a traditional decision tree structure:

Root Knot
├── Edge: If condition A → Knot A
├── Edge: If condition B → Knot B
└── Edge: Default → Default Knot

This structure is efficient for simple decisions with a small number of outcomes.

Two-Level Trees

For configuration selection, consider a two-level tree structure:

Root Knot
├── Edge: If condition A → Config A Knot
├── Edge: If condition B → Config B Knot
└── Edge: Default → Default Config Knot

This structure is efficient for selecting one of several configurations based on conditions.

Segmentation Trees

For user segmentation, consider a segmentation tree structure:

Root Knot
├── Edge: If user is premium → Premium Segment Knot
│   ├── Edge: If user is new → New Premium User Knot
│   └── Edge: Default → Regular Premium User Knot
├── Edge: If user is free → Free Segment Knot
│   ├── Edge: If user is new → New Free User Knot
│   └── Edge: Default → Regular Free User Knot
└── Edge: Default → Unknown Segment Knot

This structure efficiently segments users into groups and subgroups.

Feature Flag Trees

For feature flags, consider a feature flag tree structure:

Root Knot (Map)
├── "feature1" → Feature 1 Knot
│   ├── Edge: If user is in beta → Enabled Knot
│   └── Edge: Default → Disabled Knot
├── "feature2" → Feature 2 Knot
│   ├── Edge: If user is premium → Enabled Knot
│   └── Edge: Default → Disabled Knot
└── "feature3" → Feature 3 Knot
    ├── Edge: If user is in region A → Enabled Knot
    └── Edge: Default → Disabled Knot

This structure efficiently manages multiple feature flags.

Reusing Knots

Reusing knots can reduce memory usage and improve cache efficiency:

  • Identify common subtrees: Look for patterns that repeat in your tree structure
  • Extract common subtrees: Create separate knots for common subtrees
  • Reference common knots: Use the same knot in multiple places

Example of reusing knots:

// Create common knots
Knot enabledKnot = bonsai.createKnot(
    ValuedKnotData.builder().booleanValue(true).build(),
    Map.of("description", "Feature enabled")
);

Knot disabledKnot = bonsai.createKnot(
    ValuedKnotData.builder().booleanValue(false).build(),
    Map.of("description", "Feature disabled")
);

// Reuse the common knots in multiple places
bonsai.addVariation(feature1Knot.getId(), Variation.builder()
    .knotId(enabledKnot.getId())
    .filters(List.of(
        Filter.builder()
            .path("$.user.beta")
            .operator(Operator.EQUALS)
            .value(true)
            .build()
    ))
    .build());

bonsai.addVariation(feature1Knot.getId(), Variation.builder()
    .knotId(disabledKnot.getId())
    .filters(List.of())
    .build());

bonsai.addVariation(feature2Knot.getId(), Variation.builder()
    .knotId(enabledKnot.getId())
    .filters(List.of(
        Filter.builder()
            .path("$.user.premium")
            .operator(Operator.EQUALS)
            .value(true)
            .build()
    ))
    .build());

bonsai.addVariation(feature2Knot.getId(), Variation.builder()
    .knotId(disabledKnot.getId())
    .filters(List.of())
    .build());

Context Structure

The structure of your context data can also affect performance:

  • Keep context data small: Include only the necessary data in the context
  • Structure context data for efficient access: Organize data to minimize the complexity of JsonPath expressions
  • Flatten deeply nested structures: Consider flattening deeply nested structures to simplify JsonPath expressions

Example of restructuring context data:

// Instead of this deeply nested structure
{
    "user": {
        "preferences": {
            "settings": {
                "theme": {
                    "color": "dark"
                }
            }
        }
    }
}

// Consider this flatter structure
{
    "user": {
        "theme": "dark"
    }
}

Measuring and Testing

To optimize your tree structure effectively:

  • Measure baseline performance: Establish a baseline for comparison
  • Test with realistic data: Use representative data for testing
  • Isolate changes: Test one optimization at a time
  • Measure the impact: Quantify the performance improvement of each optimization
  • Consider trade-offs: Balance performance with maintainability and readability

Best Practices

  • Start simple: Begin with a simple tree structure and optimize as needed
  • Focus on hot paths: Optimize the most frequently used paths first
  • Consider the full lifecycle: Balance creation, evaluation, and maintenance costs
  • Document your design decisions: Document why you structured the tree a certain way
  • Review and refactor: Periodically review and refactor your tree structure
  • Test thoroughly: Test your optimizations with various inputs and edge cases