Level 11 /

Reading Connector Code

Step back and look at what you have built. The station is complete, the train is assembled, the system works. Gopher stands on the platform, surveying the whole line: Appville visible in one direction, Conductor City in the other, and Baton Junction right here in between.

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:

FileRoleSDK Interface
resource_types.goDeclares user, group, role types-
helpers.goPagination, status mapping, display names-
connector.goCentral struct, metadata, validation, actionsConnectorBuilderV2
users.goUser sync, account creation, profile actionsResourceSyncerV2 + AccountManager + ResourceActions
groups.goGroup sync, membership grants, provisioningResourceSyncerV2 + ResourceProvisioner
roles.goRole sync (identical pattern to groups)ResourceSyncerV2 + ResourceProvisioner
actions.goEnable/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:

  1. Parse the page token with parsePageToken
  2. Fetch one page from the API
  3. Convert results in a for range loop
  4. Advance the bag with bag.Next
  5. 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.

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!

Explore the related connector code files

Next Lesson

What's Next

Continue