Skip to content

Conversation

@aengelberg
Copy link

@aengelberg aengelberg commented Jul 9, 2025

Problem

go-sdk-common currently offers a Builder and MultiBuilder, to create single-kind and multi-kind contexts, respectively. These offerings have the following limitations:

  • No way to create a builder from an existing multi-context, to make further updates
  • No way to update or replace values in sub-contexts, while building a multi-context

The reason this matters is that we want to encourage incrementally building multi-contexts and passing them around, particularly in server-side codebases. However, to do that requires the ability to easily deep-copy a multi context, and make further updates to new kinds you want to add.

Solution

Introduce a new ContextBuilder type and associated constructor methods NewContextBuilder, NewContextFromContextBuilder. The intent is to then deprecate NewBuilder and NewMultiBuilder in favor of this new thing.

New syntax

The new ContextBuilder offers a Rust-inspired pattern for building multiple contexts in a single chain.

// current state (using MultiBuilder and Builder)
context := ldcontext.NewMultiBuilder().
	Add(ldcontext.NewBuilder("user-key").
		Name("my-name").
		SetString("country", "us").
		Build()).
	Add(ldcontext.NewBuilder("org-key").
		Kind("org").
		SetString("type", "enterprise").
		Build()).
	Build()
// new state (using ContextBuilder)
context := ldcontext.NewContextBuilder().
	Kind("user", "user-key").
	Name("my-name").
	SetString("country", "us").
	Kind("org", "org-key").
	SetString("type", "enterprise").
	Build()

Calling Kind both establishes an inner context with the desired kind and key if it doesn't exist yet, and returns a KindBuilder that can be used to set more attributes on that inner context. KindBuilders can be chained - calling Kind on a KindBuilder returns a new KindBuilder. Calling Build on any KindBuilder will return the whole (multi)context.

Deprecation

MultiBuilder, NewMultiBuilder, and NewMulti are now deprecated.

Builder and friends aren't deprecated yet because:

  • ContextBuilder uses Builder structs under the hood for holding individual contexts' data
  • NewBuilder and NewBuilderFromContext are used by some old lduser stuff, so the linter complains

The intent is to remove Builder and MultiBuilder in the next major version of the Go Server SDK and Go Client SDK.

Performance

It's worth noting that performance-wise, ContextBuilder is probably inferior to both Builder and MultiBuilder because it allocates more memory and generates more garbage. This would probably be felt more by customers switching over from Builder, since Builder was very well optimized for single-kind contexts. There's probably room for some perf optimizations that do fewer allocations in single-kind situations.

Testing

New unit tests dedicated to the new ContextBuilder. A lot of ContextBuilder's implementation utilizes the now-deprecated Builder under the hood, so a lot of the new unit tests duplicate existing tests for the old builder. However, these duplicate tests mean that we can eventually rewrite the new code to share less of the old code, and then remove the old code.

// SINGLE KIND TESTS (adapted from builder_simple_test.go)
// A lot of these tests are redundant because they test code that just calls through to a Builder anyway.
// However, duplicating the tests means we can potentially rewrite KindBuilder to not use Builder at all, then deprecate
// and remove the old Builder.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the tests from here down are fairly trivial copies from existing tests in builder_simple_test.go, but they use the new ContextBuilder, treating it as a builder of a single kind.

})
}

// MULTI KIND TESTS (loosely adapted from builder_multi_test.go)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests from here down are newer / more handcrafted by me, because the multi-kind logic of ContextBuilder is somewhat different than what MultiBuilder can do.

Comment on lines +120 to +135
// Add adds one or more contexts to a ContextBuilder. Only one context of each kind is allowed.
// If multiple contexts are added with the same kind, the last one fully replaces the previous ones.
func (cb *ContextBuilder) Add(contexts ...Context) *ContextBuilder {
for _, c := range contexts {
if c.Multiple() {
for _, ic := range c.multiContexts {
cb.Add(ic)
}
} else {
b := &Builder{}
b.copyFrom(c)
cb.singleBuilders[c.Kind()] = b
}
}
return cb
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something like this is necessary for ContextBuilder to be a full functional replacement to MultiBuilder. In general it's good to be able to have helper functions return individual contexts of different kinds, and some higher level thing can merge them all together.

One notable weird thing about this Add is that it has totally different context-kind-uniqueness rules as MultiBuilder's Add. This one doesn't care if you add duplicate kinds, and the last one wins. I think that's much simpler so we don't have to deal with a new error case, but the discrepancy could be confusing.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With relatively little context (haha), I think last-one-wins sounds a lot more intuitive than raising an error!

@aengelberg aengelberg marked this pull request as ready for review July 15, 2025 23:44
@aengelberg aengelberg requested a review from a team as a code owner July 15, 2025 23:44
@aengelberg
Copy link
Author

Closing this since I'm no longer prioritizing putting this out there as part of the context propagation project, but I encourage reviving this if we're trying to improve Go ergonomics in general

@aengelberg aengelberg closed this Jul 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants