Gopher says
Reading code is a skill. Most developers spend far more time reading code than writing it - especially when joining a project or debugging production issues.
In baton-junction, we’re going to walk through the entire connector - every file, every pattern. Once you can read one connector, you can read them all.
The File Map
Here’s every file in pkg/connector/ and what it does:
| File | Role | SDK Interface |
|---|---|---|
resource_types.go | Declares user, group, role types | - |
helpers.go | Pagination, status mapping, display names | - |
connector.go | Central struct, metadata, validation, actions | ConnectorBuilderV2 |
users.go | User sync, account creation, profile actions | ResourceSyncerV2 + AccountManager + ResourceActions |
groups.go | Group sync, membership grants, provisioning | ResourceSyncerV2 + ResourceProvisioner |
roles.go | Role sync (identical pattern to groups) | ResourceSyncerV2 + ResourceProvisioner |
actions.go | Enable/disable user, profile update handlers | - |
Data Flow
Once you know how data flows through a connector, you can follow any Baton connector’s logic - the same pipeline appears everywhere. Data moves through the connector in a predictable pipeline:
Every builder follows the same five-step pagination skeleton:
- Parse the page token with
parsePageToken - Fetch one page from the API
- Convert results in a
for rangeloop - Advance the bag with
bag.Next - Marshal and return the next page token
Patterns That Repeat
Connector code follows a few recurring patterns. Once you recognize them, the codebase reads faster.
Constructor pattern - every package exposes a single, consistent way to create its main type. You’ll see a New function returning (*Type, error):
func New(ctx context.Context, cc *cfg.App, opts *cli.ConnectorOpts) (connectorbuilder.ConnectorBuilderV2, []connectorbuilder.Opt, error)
func newUserBuilder(c *client.Client) *userBuilder
func newGroupBuilder(c *client.Client) *groupBuilder
Error wrapping - errors bubble up with context so you always know where they originated. Every error includes a baton-junction: prefix with %w. At the call site, this is paired with structured zap logging (the full pattern you saw in Level 2):
return nil, fmt.Errorf("baton-junction: failed to list users: %w", err)
Context threading - ctx flows from main() through the SDK, into builders, into client methods, into HTTP requests. Every function that does I/O takes ctx as its first parameter.
Repetition over abstraction - groups and roles are nearly identical files. Each is self-contained and readable without cross-referencing the other.
A Python developer might extract a shared base class for groups and roles. Go connectors prefer independence: each builder file reads top-to-bottom without jumping to shared abstractions. If roles later need different behavior, the files diverge naturally.
Entitlements vs Grants
Users don’t have entitlements - they’re the recipients. Groups and roles have entitlements (like “member”) that get granted to users:
// Users: empty implementations (interface requires them)
func (o *userBuilder) Entitlements(...) { return nil, nil, nil }
func (o *userBuilder) Grants(...) { return nil, nil, nil }
// Groups: implementations
func (o *groupBuilder) Entitlements(...) {
e := entitlement.NewAssignmentEntitlement(res, memberEntitlement, ...)
return []*v2.Entitlement{e}, nil, nil
}
Provisioning: Grant & Revoke
Grant adds a membership. Revoke removes one - but treats “already removed” (404) as success:
func (o *groupBuilder) Revoke(ctx context.Context, g *v2.Grant) (annotations.Annotations, error) {
l := ctxzap.Extract(ctx)
groupID := g.Entitlement.Resource.Id.Resource
userID := g.Principal.Id.Resource
l.Info("revoking group membership",
zap.String("group_id", groupID), zap.String("user_id", userID))
if err := o.client.RemoveGroupMember(ctx, groupID, userID); err != nil {
if isNotFound(err) {
l.Debug("group membership already removed",
zap.String("group_id", groupID), zap.String("user_id", userID))
return nil, nil
}
return nil, fmt.Errorf("baton-junction: failed to revoke: %w", err)
}
return nil, nil
}
For each file listed below, write the SDK interface it implements next to the →. Use none for files that don’t implement an interface. Some files implement multiple interfaces - list them all. The options are: ConnectorBuilderV2, ResourceSyncerV2, ResourceProvisioner, AccountManager, none.
Number each step from 1 to 5 to show the correct execution order when the SDK calls groupBuilder.Grants().
Gopher says
Every file follows the same patterns - constructors, error wrapping, context threading, pagination skeletons. Repetition is the point: it makes code predictable.
In baton-junction, once you see the pattern in one builder, you can read any Baton connector in the world. The architecture is deliberately uniform.
Next: the Part 1 review!
Next Lesson
What's Next

