Category: Golang

  • Further work on my Three Tier Go Wide-Game-Bot

    I’ve had less time over Christmas to work on it, but am progressing. My relationship with Claude Code is turning more supervisory. I’m often generating sample code and infrastructure/patterns for it to follow. “Make me a new Team Store like my User Store”.

    This way I have set up the working pattern for Domain, Store, Busines Layer and Presentation. I now have a strategy which I believe is more maintainable and allows for testing.

    The Domain Layer – Static Stores

    The first infrastructure made by Claude had used dependency injection to pass store interfaces down through the layers. This resulted in a StoreProvider interface to reduce the amount of parameters to pass around, and a lot of effort. Changes I’ve made:

    • My Stores are packages full of static methods.
    • My CommandContext interface has been broken out into specific HasGormTransaction and HasRedisConnection. The stores are coded to accept the one (or both) they need. This makes testing easier. (In my first article, Experiments in Go – Three Tier Architecture in a WhatsApp Game-Bot, I discussed how I use a Command Pattern to place control of transaction boundaries outside the business functions)
    • I have a library of support code to make working with Gorm consistent and easier.

    The idea is that the business layer should not be dealing with Gorm directly.

    An example Store

    This is the header of the User Store and the first method, GetById. The majority of the work is performed by the lower layer stores package. I’ve wrapped the Not Found error into a more specific userstore.ErrNotFound`. That’s likely the most complex piece of code here.

    Go
    var (
    	ErrNotFound = errors.New("user not found")
    )
    
    type QueryOption = stores.QueryOption[domain.User]
    type PreloadOption = stores.PreloadOption
    
    func WithPreloadCurrentGame(options ...PreloadOption) QueryOption {
    	return stores.WithPreload[domain.User]("CurrentGame", options)
    }
    
    func WithPreloadLastControl(options ...PreloadOption) QueryOption {
    	return stores.WithPreload[domain.User]("LastControl", options)
    }
    
    func WithPreloadLastControlLocation(options ...PreloadOption) QueryOption {
    	return stores.WithPreload[domain.User]("LastControlLocation", options)
    }
    
    // GetById returns a user by ID. Returns ErrNotFound if the user is not found.
    func GetById(ctx cmdcontext.HasGormTransaction, userId uuid.UUID, options ...QueryOption) (*domain.User, error) {
    	allOptions := append([]QueryOption{stores.Where[domain.User]("id = ?", userId)}, options...)
    	user, err := stores.First[domain.User](ctx, allOptions)
    	if err != nil {
    		if errors.Is(err, gorm.ErrRecordNotFound) {
    			return nil, ErrNotFound
    		}
    		return nil, fmt.Errorf("get user: %w", err)
    	}
    	return user, nil
    }

    The store also provides methods to mutate the User. I’ve had to use the non-generic Gorm API here because the generic API does not make it easy to update set fields. There’s an issue raised against Gorm about this problem.

    Go
    // SetContextToPlaying sets the user's current playing context to playing the given game.
    // This store function does not validate that the user is a player of the game.
    func SetContextToPlaying(ctx cmdcontext.HasGormTransaction, userId uuid.UUID, gameId int64) error {
    	updates := map[string]interface{}{
    		"current_context": cmdcontext.UserContextPlaying,
    		"current_game_id": gameId,
    	}
    
    	err := ctx.GormDB().Model(&domain.User{}).
    		Where("id = ?", userId).
    		Updates(updates).Error
    
    	if err != nil {
    		return fmt.Errorf("set context to playing: %w", err)
    	}
    
    	return nil
    }
    

    How the magic works – the Stores package

    Gorm’s generics interface is more typesafe. This includes different interfaces (gorm.Interface[T] and gorm.ChainInterface[T]) at different stages of the query building process. This makes using the Options Pattern harder. Here’s my Options structure:

    Go
    type QueryOptions[T any] struct {
    	FirstOption FirstOptionBuilder[T]
    	Options     []ChainedOptionBuilder[T]
    	Clauses     []clause.Expression
    }
    
    type FirstOptionBuilder[T any] func(db gorm.Interface[T]) gorm.ChainInterface[T]
    type ChainedOptionBuilder[T any] func(db gorm.ChainInterface[T]) gorm.ChainInterface[T]
    
    func NewQueryOptions[T any]() *QueryOptions[T] {
    	return &QueryOptions[T]{
    		FirstOption: nil,
    		Options:     make([]ChainedOptionBuilder[T], 0),
    		Clauses:     make([]clause.Expression, 0),
    	}
    }
    
    func BuildQueryOptions[T any](options []QueryOption[T]) *QueryOptions[T] {
    	result := NewQueryOptions[T]()
    	for _, opt := range options {
    		opt(result)
    	}
    	return result
    }
    
    func (q *QueryOptions[T]) HasFirstOption() bool {
    	return q.FirstOption != nil
    }
    
    func (q *QueryOptions[T]) AddChainedOption(opt ChainedOptionBuilder[T]) {
    	q.Options = append(q.Options, opt)
    }
    
    func (q *QueryOptions[T]) AddClause(c clause.Expression) {
    	q.Clauses = append(q.Clauses, c)
    }

    This results in options that have to be more intelligent about the kind of Builder that they create. Here’s the Where option

    Go
    // Where creates a Query that applies a WHERE clause to the query
    func Where[T any](where string, args ...interface{}) QueryOption[T] {
    	return func(opts *QueryOptions[T]) {
    		if !opts.HasFirstOption() {
    			opts.FirstOption = func(db gorm.Interface[T]) gorm.ChainInterface[T] {
    				return db.Where(where, args...)
    			}
    		} else {
    			opts.AddChainedOption(func(db gorm.ChainInterface[T]) gorm.ChainInterface[T] {
    				return db.Where(where, args...)
    			})
    		}
    	}
    }

    I also have options for locking and preload. Preload takes a varadic list of PreloadOptions to customise preload.

    One of the stores.Query method is

    Go
    // BuildQuery builds a query from the given options
    func BuildQuery[T any](ctx cmdcontext.HasGormTransaction, options []QueryOption[T]) (gorm.ChainInterface[T], error) {
    	var db gorm.ChainInterface[T]
    	opts := BuildQueryOptions[T](options)
    	for _, opt := range options {
    		opt(opts)
    	}
    	if !opts.HasFirstOption() {
    		return nil, fmt.Errorf("stores supporting layer does not support unbounded queries")
    	}
    	db = opts.FirstOption(gorm.G[T](ctx.GormDB(), opts.Clauses...))
    	for _, opt := range opts.Options {
    		db = opt(db)
    	}
    	return db, nil
    }
    
    // First executes a query and returns the first result
    func First[T any](ctx cmdcontext.HasGormTransaction, options []QueryOption[T]) (*T, error) {
    	db, err := BuildQuery(ctx, options)
    	if err != nil {
    		return nil, err
    	}
    
    	var record T
    	record, err = db.First(ctx.Context())
    	if err != nil {
    		return nil, err
    	}
    
    	return &record, nil
    }

    I wonder why Gorm copies structs all over the place, so I return a pointer. The error about unbounded queries is forced because this method would have to return a different interface depending on whether any options had been given. All of my cases provide at least a Where option so I won’t hit this error.

    How it looks in the business layer

    This is possibly one of my most complex business calls and is part of the Player Leaves Team business method. I need the player’s current team. I also need to know how many other players the team has so that when this player leaves the team I know whether to withdraw the team from the game.

    I also load the game here so that I have information about the game to report back to the user. This business method returns a structure which contains information such as the new team status (playing/withdrawn/completed), any penalty the team will suffer for not completing the game and the team and game names for use in user facing messages.

    Go
    // Load the team with members (excluding me) and game info
    team, err := teamstore.GetByUserAndGame(ctx, userId, gameId,
    		teamstore.WithPreloadMembers(
    			stores.WhereP("user_id <> ? AND status=?", userId, domain.MembershipStatusActive)),
    		teamstore.WithPreloadGame())

    I could write a function in teamstore to WithPreloadActiveMembersOtherThan(myUid). This may be a good idea as information about field names has leaked into the business layer here.

    Go
    // Load the team with members (excluding me) and game info
    team, err := teamstore.GetByUserAndGame(ctx, userId, gameId,
    		teamstore.WithPreloadActiveMembersOtherThan(userId),
    		teamstore.WithPreloadGame())

    Unit Tests with Mock Database

    The static stores mean that I cannot mock out the store layer any more. I need to test against a database. I’m using the in-memory mysql driver to do this. It’s faster, but it risks errors due to differences between the mysql driver and the real Postgresql database. The biggest risk is constraints.

    The core infrastructure is a builder

    Go
    type SetupCommand interface {
    	Name() string
    	Command() Visitor
    }
    
    type Builder interface {
    	// WithFurtherSetup creates a new builder with the given setup commands in addition to the existing ones
    	// The existing builder is not modified
    	WithFurtherSetup(...SetupCommand) Builder
    
    	// Build creates a new MockDatabase instance
    	Build(t *testing.T) MockDatabase
    }

    Two builder types exist. The Root Builder (rootBuilder is private to this package) has only the list of commands. The Chained Builder has a parent Builder and more commands. This allows me to create a Builder to create the schema I need and another Builder which adds to this with test or test group specific data.

    In practice it looks something like this:

    Go
    // testBuilder creates a builder with all necessary tables for user store tests
    var testBuilder = dbmock.NewBuilder(
    	dbmock.WithUserTable(),
    	dbmock.WithGamesTable(),
    )
    
    var seededTestBuilder = testBuilder.WithFurtherSetup(
    	// Seed data using command builder pattern
    	dbmock.NewCreateUserCommand("user1", func(u *domain.User) {
    		u.PhoneNumber = dbmock.StringPtr("+1234567890")
    		u.DisplayName = dbmock.StringPtr("Test User")
    	}),
    	dbmock.NewCreateUserCommand("user2", func(u *domain.User) {
    		u.PhoneNumber = dbmock.StringPtr("+9876543210")
    		u.DisplayName = dbmock.StringPtr("Second User")
    	}),
    	dbmock.NewCreateGameCommand("game1", "user1", func(g *domain.Game) {
    		g.Title = "Test Game"
    		g.GameCode = dbmock.StringPtr("TEST001")
    		g.Status = domain.GameStatusActive
    	}),
    )

    The test then uses the builders:

    Go
    func TestGetById_BasicLoad(t *testing.T) {
    	// Setup
    	mockDB := seededTestBuilder.Build(t)
    	ctx := mockDB.NewContext()
    	userID := mockDB.GetValue("user1.ID").(uuid.UUID)
    
    	// Execute
    	user, err := userstore.GetById(ctx, userID)
    
    	// Assert
    	require.NoError(t, err)
    	require.NotNil(t, user)
    	assert.Equal(t, userID, user.ID)
    	assert.Equal(t, "+1234567890", *user.PhoneNumber)
    	assert.Equal(t, "Test User", *user.DisplayName)
    	assert.Equal(t, cmdcontext.UserContextInactive, user.CurrentContext)
    	//...

    It’s more useful to look at the Team Member setup command, as this shows how commands depend on eachother.

    Go
    // CreateTeamMember creates a test team member with default values
    func CreateTeamMember(db MockDatabase, teamID int64, userID uuid.UUID, overrides ...func(*domain.TeamMember)) *domain.TeamMember {
    	member := &domain.TeamMember{
    		TeamID:   teamID,
    		UserID:   userID,
    		Status:   domain.MembershipStatusActive,
    		IsLeader: false,
    		JoinedAt: time.Now(),
    	}
    
    	for _, override := range overrides {
    		override(member)
    	}
    
    	err := db.DB().Create(member).Error
    	require.NoError(db.T(), err, "failed to create test team member")
    	return member
    }
    
    func NewCreateTeamMemberCommand(key string, teamKey string, userKey string, overrides ...func(*domain.TeamMember)) SetupCommand {
    	return NewSetupCommand(fmt.Sprintf("create team member %s", key), func(db MockDatabase) error {
    		teamID := db.GetValue(teamKey + ".ID").(int64)
    		userID := db.GetValue(userKey + ".ID").(uuid.UUID)
    		member := CreateTeamMember(db, teamID, userID, overrides...)
    		db.SetValue(key, member)
    		db.SetValue(key+".ID", member.ID)
    		return nil
    	})
    }
    

    The pattern of storing key and key.ID allows joins to work.

    I’ve used test helper systems in work and they really do thelp. The system I wrote at work for a large project went a layer beyond this. I could ask for any business object and the system would default all of its dependencies for me, so here I’d ask for a Team Member and, unless I overrode them, I’d get a User and a Team and a Game automatically. Here I need to set up the User, Team and Game but this is not hard. The test helper framework is quite simple as a result.

    Business Layer – Back to Command Objects

    My business layer has returned to using Command Objects over Command Functions. To be fair, I could have likely stuck with functions. The use of objects has allowed a simple mocking framework for presentation layer functions that call a single business method.

    Go
    var mocks = make(map[reflect.Type]any)
    
    // GetMock returns a mock of the given type and returns true if it exists
    func GetMock[T any]() (T, bool) {
    	typeT := reflect.TypeOf((*T)(nil)).Elem()
    	mock, ok := mocks[typeT].(T)
    	return mock, ok
    }
    
    // SetMock sets a mock of the given type
    func SetMock[T any](mock T) {
    	mocks[reflect.TypeOf((*T)(nil)).Elem()] = mock
    }
    
    // ClearMocks removes all registered mocks
    // Should be called in test cleanup (defer) to prevent test pollution
    func ClearMocks() {
    	mocks = make(map[reflect.Type]any)
    }

    My business function uses a factory method to create the Command instance. For example

    Go
    // LeaveTeamCommand removes the player from the team and returns information about the new state of the team.
    // It will execute team state transitions as a result of the last player leaving.
    // It will execute user context state transitions, setting the user to idle if they are currently playing in this team.
    type LeaveTeamCommand interface {
    	cmdcontext.Command
    	Result() *LeaveTeamResponse
    }
    
    // NewLeaveTeamCommand constructs a New LeaveTeamCommand for the given user and game.
    // It will return a mock instance if one is set.
    func NewLeaveTeamCommand(userId uuid.UUID, gameId int64) LeaveTeamCommand {
    	mock, isMocked := cmdcontext.GetMock[LeaveTeamCommand]()
    	if isMocked {
    		return mock
    	}
    	return &leaveTeamCommand{
    		userId: userId,
    		gameId: gameId,
    	}
    }
    

    The mock is not aware of the parameters passed here. I could improve the framework with a factory pattern that accepts the parameter list and returns a mock instance, or support assertion to capture the parameters. This can all come. For now this simple framework allows me to test my presentation layer without the mock database if I want.

  • Go Config Tools

    Go Config Tools

    Both in my professional work and mow my own projects I’ve seen Claude makde huge complicated CONFIG loader routines. Wouldn’t it be great if I could load configuration from the environment just as I unmarshal a JSON structure. The code would be so much easier to read and maintain.

    So I’ve done just this at https://github.com/m0rjc/goconfigtools.

    I’m adding capabilities to custom validate and, I expect, custom marshal values. This will give very nice compact configuration for my Kubernetes projects both at home and at work.

    As an aside – Claude didn’t understand that you could add a varags parameter to a method without breaking existing clients. This explains why goaitools has methods like NewClientWithOptions() rather than just NewClient() with the additional varargs.

  • Writing the Party-Bot

    Writing the Party-Bot

    This will be impressive if it works! Using Claude to write a party food organising bot on top of my wide-game-bot framework!

    I’ve extended the framework to allow a Game Type to choose the “command routing” taken by the system. It already had fixed command routers depending on player context, “Admin”, “Player”, “Idle” and “Anonymous”. Now I add a concept of a RoutePair which means that my wide games will go to “location-base-game/admin” and “location-based-game/player“. These remain the same, but I add a new “party/admin” and “party/player“.

    These routes both inherit the “always” route which allows for joining and leaving a game, and then implement only an AI Assistant. The assistant has tools to get the party menu and allow a player, er…., participant to specify what they are bringing.

    Players Participants can only see their own food and that someone else is bringing crisps and nibbles. The game admin can see the entire menu alongside who is bringing what.

    This is a great thing about Strategy Pattern. I’ve just added a new Game Type Strategy which plays a very different game to anything I imagined when I started this project.

    Findings as I implement this

    Teams!

    The existing games are in teams. The architecture assumes that a player is part of a team and a team is in a game. This means that party participants also have to be in teams! This could work well. “The Smith Family” could have both parents contributing to the same food list. It also means that a participant chooses their display name. I was going to use their WhatsApp display name. The system uses AI to moderate these. I’ll have to have the AI moderate their food choices too!

    Other people’s ideas?

    I imagine other people are solving the problems I’m solving. I’ve never had a SimpleToolAction, but the idea here is interesting and maybe I can learn from it.

    I should have stuck to Stores

    I discussed store pattern in Experiments in Go – Three Tier Architecture in a WhatsApp Game-Bot and decided to move from a Store pattern with manual SQL to using Gorm directly in code. I’m learning now that I should have stuck to Stores, and will write up more on this later.

    Claude keeps looking for Stores. I don’t know if that’s from remains of documentation about the pattern in the codebase docs folder, or because everyone does Stores. The final architecture would be Stores with Gorm based store implementations but that will be a large refactor (oh no not again!). I’m finding the domain package is filling up with Store methods. I also want to be able to swap in things like a REDIS store for objects like the mementos used to track multi-step user conversations.

    A Prompt Injection Attack! (Sort of)

    I’d initially intented it not to be possible for participants to see who was bringing what. In retrospect this was an anti-feature. People are discussing this over WhatsApp in the Parents Channel. When I wrote the wide-game-bot I’d designed the player tools to make it impossible for the AI to do anything the player should not do or access information that the player cannot access. Claude’s rapidly written “list_food” tool returns the food items along with the name of the teams that bring them.

    I noticed this when a parent asked me to add a food item on their behalf. So I added specifically “Crisps (Eliot)”. The AI then started including team names for all of the other items in brackets when listing foodstuffs!

    So my food item was

    JSON
    {
       "food":"Crisps (Eliot)",
       "team":"Sparks and James"
     }

    Every other item was

    JSON
    {
      "food":"Drinks",
      "team":"The Smiths"
    }

    The AI formatted that one as “Drinks (The Smiths)“.

    It’s an easy bug to fix, but I’ve not fixed it because I think it’s actually a feature. If Claude had followed my original intent and prevented acccess then I’d have been tempted to add it in!

    My wide game players still cannot see whether territory is owned by any other teams before entering it and risking a penalty, or in the simple Score game they cannot see which bases have been claimed already.

    Conclusion

    I have a party-bot. It was developed very quickly, a few hours including refactoring the Game Strategy to allow complete user input routing override through the existing Command Router System and debugging.

    Testing the new bot as an admin using my Simulator test-harness
    Testing the bot using my simulator test-harness command.

    I have a lot of technical debt and a large TODO file to clean it all up.

    I’m about to test this all for real by sharing it with my Scout parents!

    Is Conversational AI the new User Interface?

    Using the live system!

    I’ve got used to it and find it natural. Will my users? That’s a big question. This could be crying out for a quick website solution, something which I may well add! Then I’ll have a web site and WhatsApp user interface to the same backend data which will be neat.

    I can imagine a world in which conversational UI is the norm. It seems full circle, back to the command line, but with voice recognition. “Hey – Party-Bot – We’re bringing cake!” – possible now if someone presses the “voice input” button on their phone.

    The web site would also have a place. This is a problem that just calls for tabular data and an “add row” button.

    References

    This is the AI Assistant supporting code from the project:
    https://github.com/m0rjc/goaitools

    I hope to publish the WhatsApp ingress and egress code when it is in some kind of shape to be shared. At the moment it has no egress rate limiting, which is a risk. The chance of me exceeding WhatsApp limits with a small party of Scout parents is low, so I take that risk.

  • Strategy Pattern is your friend

    Strategy Pattern is your friend

    I’ll start with Claude’s conclusion after I asked for the refactor:

     Perfect! All tests pass. This is a huge improvement - the Territory config now uses semantically meaningful names:
    
      What We Accomplished
    
      Before (confusing):
      {
        "first_visitor_points": 100,      // Wait, do I gain or lose points?
        "subsequent_visitor_points": -20   // Negative means... rent? Cost? Loss?
      }
    
      After (crystal clear):
      {
        "ownership_cost": 100,  // Cost to claim - I pay 100 points
        "visit_cost": 20        // Cost to visit - I pay 20 points (goes to owner)
      }
    
      Key Benefits:
      1. ✅ No more sign confusion - all values are positive costs
      2. ✅ Semantic clarity - "cost" clearly means points you pay
      3. ✅ Strategy handles signs - the Territory strategy converts costs to negative when applying them
    
      This is the power of the Strategy Pattern - we can have domain-appropriate terminology for each game type! Score
      Attack uses "points" (gains), Territory uses "costs" (losses). Beautiful! 🎉
    

    Before Refactor

    My WhatsApp Urban Wide-Game Bot had a game table and controls table, both of which stored scoring information in two fields:

    • First Visitor Points
    • Subsequent Visitor Points

    This reduced flexibility. What if I want a depreciating list, for example [30,20,15,10,5,0,0,0,0.....]? How do I store that? It also produced confusion. One of my current game types, which Claude has insisted on calling “Score Attack”, is like a traditional Score Orienteering competition. Players gain points for visits. The other, more often played by the Scouts, has an ownership semantic. It costs points to claim territory, but then you earn points whenever another team visits.

    A big issue I had throughout the code was getting myself (and Claude) to understand whether First Visitor Points and Subsequent Visitor Points should be positive or negative and what the signs meant. Now I have different game strategies each with their own JSON configuration in the database I can make the fields meaningful.

    After Refactor

    The entire knowledge of how to play each game type is contained in its Strategy implementation. That is the beauty of the Strategy Pattern. Anything I want to know about a Territory Game is in one place.

    Another advantage of Strategy, a traditional advantage, is that I can split off behaviours into different strategies. Let’s say I wanted different types of game ending? I could have an Ending Strategy. I could have a Team Signup Strategy. I then combine my selection into a single game.

    Here’s the Strategy Interface so far:

    Go
    // Strategy defines the interface for game type behavior
    type Strategy interface {
    	// Key returns a unique identifier for the game type
    	Key() domain.GameType
    
    	// Name returns the name of the game type as would be displayed to the user.
    	Name() string
    
    	// GameMechanicsText returns a description of the game mechanics for use in an AI assistant
    	GameMechanicsText() string
    
    	// DefaultConfig returns the default scoring configuration for this game type.
    	// This is used when creating new games or when config parsing fails.
    	DefaultConfig() ScoringConfig
    
    	// ParseConfig parses a JSON configuration into a typed config struct.
    	// Returns the strategy's default config on parse errors (graceful degradation).
    	// Handles unknown fields gracefully for backward compatibility.
    	ParseConfig(config json.RawMessage) (ScoringConfig, error)
    
    	// ConfigSchema returns a JSON schema describing the configuration fields
    	// for this game type. Used to dynamically generate AI tool schemas.
    	// Only includes semantic fields (not internal fields like version).
    	ConfigSchema() map[string]interface{}
    
    	// MergeConfig merges partial configuration updates with the current config.
    	// Used by AI tools to apply partial updates without replacing the entire config.
    	// Returns the complete merged configuration as JSON.
    	MergeConfig(current json.RawMessage, updates map[string]interface{}) (json.RawMessage, error)
    
    	// FormatScoringDescription returns a human-readable one-line description
    	// of the scoring configuration. Used in AI assistant context and game info displays.
    	// Example: "First visitor=+50 pts | Subsequent visitor=+10 pts"
    	FormatScoringDescription(config ScoringConfig) string
    }

    I’ll add a HandleVisit() method when I implement game play. This still needs to be refined. It cannot know about WhatsApp, so must communicate its reply in terms of who is awarded what score. Then the business layer would apply the scores and the presentation layer will inform the users through their appropriate channels. All this to come!

    To show the variation, here’s the AI Tool schema for the Territory Strategy

    Go
    // ConfigSchema returns the JSON schema for Territory configuration
    func (s *TerritoryStrategy) ConfigSchema() map[string]interface{} {
    	return map[string]interface{}{
    		"type": "object",
    		"properties": map[string]interface{}{
    			"ownership_cost": map[string]interface{}{
    				"type":        "integer",
    				"description": "Cost in points to claim a checkpoint (positive value, will be deducted from claimer's score)",
    				"minimum":     0,
    			},
    			"visit_cost": map[string]interface{}{
    				"type":        "integer",
    				"description": "Cost in points when visiting an owned checkpoint (positive value, will be deducted from visitor and transferred to owner)",
    				"minimum":     0,
    			},
    		},
    		"required": []string{},
    	}
    }

    And here for the Score Attack (Score Orienteering) game

    Go
    func (s *ScoreAttackStrategy) ConfigSchema() map[string]interface{} {
    	return map[string]interface{}{
    		"type": "object",
    		"properties": map[string]interface{}{
    			"first_visitor_points": map[string]interface{}{
    				"type":        "integer",
    				"description": "Points awarded to the first team to visit a checkpoint",
    			},
    			"subsequent_visitor_points": map[string]interface{}{
    				"type":        "integer",
    				"description": "Points awarded to other teams visiting the checkpoint",
    			},
    		},
    		"required": []string{},
    	}
    }

  • AI Tooling – Making The Scheduler

    It’s Reimplemented Cron!

    My WhatsApp Game bot needs scheduled tasks. At first it’s database cleanup, to clear down expired mementos and conversation state. I’ll add game completion and tidying when those features are implement. The task for the AI was “Implement Scheduling”.

    It’s written a pretty comprehensive Cron implementation which is database backed.

    Would I have done this? 20 years ago working on AIX systems I’d have used Cron. Today, professionally, working with Kubernetes I’d look at Kubernete’s scheduler. It could be a quick and dirty thing to add a cron-job – every 15 minutes

    DELETE FROM mementos WHERE expires_at < NOW()

    I challenged Claude on this, and it did have a valid point. Having one binary gives me easy deployment in my early dev days. I can split out scheduling later when I need to, but for now I can push a container image to my dev server and everything will update. Management is easier.

    The mementos will likely move to REDIS, maybe something I should have already done (I was concerned with the cost of REDIS/Elasticache if I used AWS for this, but have a local dev server for now). Tasks like game ending are not so easy to do in Cron without producing a purpose made binary from the Go code.

    It did it in 15 minutes

    The scheduler took about 15 minutes to write. Interestingly it’s used a Store Pattern rather than bring GORM into the mid-tier (see my earlier post on domain layer design). This could be because it had a previous attempt as reference. This is quite fast.

    I had to ask for concurrency (and other things)

    The first version had single shot scheduled tasks which would reschedule themselves. This seemed flaky so I asked for recurring job support. The code was adapted and a call was added to main to create the jobs. This would result in a new set of jobs being added every time the program ran. It’s a bug that would be spotted if paying attention to the database, otherwise if this had been Vibe Coded then performance would have degraded over time. I asked Claude to implement an idempotent startup and to reinforce it with a database constraint. The database constraint is keyed on Job Type and a Uniqueness Key, allowing me to scope jobs.

    Claude does tell me that job taking is concurrent safe. It uses Postgres’ SKIP LOCKED feature which is a great tool.

    I need to manually review the code. Claude has run out of tokens for this morning so I have time (until the family wake up) to look at it. I’ll check that the job taking is a short transaction and state management is used. I’ll need to understand how failed jobs are handled.

    It did the tests while I went for a run

    A nice thing for the hobbyist – I left it writing the end to end tests for the system while I went for a run. This could be why it’s run out of tokens! I’ve been manually testing so far. I want to refactor the command routing infrastructure to allow me to pass continuation state to the AI Agent code and provide the AI Agent with tools to ask the user questions.

    Footnote

    On review of the code: There’s a fair amount of duplication in there – two almost identical registry classes for example. I also think it won’t work so well on single-shot jobs when I need them. I can fix these things over time, but I have more valuable things to spend time on now. I’ll ask Claude to factor the 70 or so lines of initialisation code out of main.go then move on.

    As of December 2025 I have largely reimplemented the Scheduler. I’ve used Claude to help, but I’ve shaped it more in my own style and fixed some issues it had. It’s now a lot more robust and correctly distinguishes between routine tasks and one-off tasks. I’m using routine “sweeper” tasks for things like game state transitions because creating a one-off task requires keeping its scheduled time in sync as the game changes.

  • Experiments in Go – Three Tier Architecture in a WhatsApp Game-Bot

    I am currently developing a game-bot which uses WhatsApp as its user interaction layer. This bot will implement a Scout widegame that we play in an urban environment. We currently play over WhatsApp with each team in a WhatsApp group with the leaders. The leaders respond to messages from the teams and track the game on paper – a slow and error prone system. The last game we played had me sitting in McDonalds on a Tuesday night quickly putting together a spreadsheert to track progress and keep score while the Scouts ran around (and sometimes sheltered from the rain) outside.

    The Three Tier Architecture

    The system I’m writing uses multiple presentation layers. It has a WhatsApp textual interface, a web interface (for game organisers) and maybe one day a mobile app. I’m finding that, with the help of AI Agents for natural language input, the WhatsApp interface is most promising at the moment. This already presents two presentation layers. My interface supports both “slash-commands” and natural language.

    I already need to separate business logic such as game actions from the presentation layer. My presentation layer deals only with taking instruction from the users and presenting the results. My business layer implements things like “What happens when a player reaches the finish?”

    The Domain Layer provides data access. It provides functions like “Create a player record”, “Update the score”. It presents the database in a more conceptual business language so that the business layer can be coded in terms of business concepts. Dependencies in this structure are strictly downwards. Each layer knows only about the layers below it.

    One question in this type of architecture is “Do we allow the presentation layer to directly access the domain, missing out the middle tier?” More modern systems I have used do this. It increases coupling between the layers, but avoids marshalling of data across layers. The domain layer and business layer are in the same codebase at the moment so this coupling can be made. I am allowing this coupling for simple read operations. Things would have to change if I ever move to a microservices architecture, but I expect the impact to be small.

    I will focus on my Domain Layer decisions in this post. My Mid-Tier and Presentation Layer are large topics in their own right and deserve their own series of posts. For context I am using a Command Pattern in my mid-tier to separate transaction lifecycle from business logic and allow composition of reusable business logic “commands”. Method signatures in the domain layer reflect this design. I started with Command Classes (traditional Object Orientated programming) but have moved to using Functions (lambdas), a nice feature of more modern languages.

    Attempt One – Stateless Stores, Raw SQL

    My initial pattern was based in many ways on a project I worked in in the early 2000s to help a large car manufacturer to manage its dealer network. That system had four or five main objects and a series of “Use Cases” that worked over them. The Use Cases were the Commands in the business logic layer. It was a project that drove home the reason that database tables use “meaningless” keys. The dealer’s own systems were keyed on a “Dealer Code” which was a short string of which the first character indicated the type of dealer. They restructured their dealer network so causing huge difficulties for their systems. Our system used surrogate keys and stored Dealer Name and Dealer Type as properties, so all we needed to do was accept the new data.

    My domain layer was generated quite quickly using Claude Code from a textual description of the conceptual model. Code generation of any kind fundamentally changes the cost of building domains. The car dealer project used a PERL script to generate the entire domain layer codebase from short text files with table definitions. If anything, that PERL script was more consistent than the AI solution, but also less flexible. The variation in the Claude code (even within one domain object) means that I have to pick an example. My refactor (Attempt Two below) has me paying a lot more attention to Claude’s output.

    Each domain object has its own package, for example “src/domain/game”. The domain object is in game.go. Types were used for enums:

    package game
    // ............
    
    // GameStatus represents the lifecycle state of a game
    type GameStatus string
    
    const (
    	GameStatusCreating   GameStatus = "creating"   // Configuration only, admin setting up
    	GameStatusJoining    GameStatus = "joining"    // Registration open, teams can join
    	GameStatusActive     GameStatus = "active"     // Game is live and being played
    	GameStatusCompleting GameStatus = "completing" // Game time ended, late penalty phase
    	GameStatusCompleted  GameStatus = "completed"  // Game finished, read-only
    )
    

    The domain object itself is a struct. Here are some of the fields. Note the use of sql types to handle NULLs. This is something I found significant in my GORM refactor.

    // Game represents a single instance of a game with configured rules and lifecycle
    type Game struct {
    	ID                       int64
    	Title                    string
    	GameCode                 string
    	AdminPasswordHash        string
    	Status                   GameStatus
    	GameType                 GameType
    	CreatedAt                time.Time
    	EndTime                  sql.NullTime
    	StartTime                sql.NullTime
    	JoiningWindowStart       sql.NullTime
    	CompletedAt              sql.NullTime
    	JoiningPasscode          sql.NullString
      // ......
    }

    The file store.go contains the interface definitions of methods to access the store. The domain layer is purely responsible for data access, so business logic like choosing a game code is performed by the business layer. The game creation method took a subset of the fields in a Params object. Another method, for example to change the game state, would take only the parameters it needs.

    // GameCreateParams contains parameters for creating a new game
    type GameCreateParams struct {
    	Title                    string
    	GameCode                 string
    	AdminPasswordHash        string
    	JoiningPasscode          string    // Plain text passcode for joining
    	GameType                 GameType  // Defaults to score_attack if empty
    	CreatedByUserID          uuid.UUID // ID of user creating the game
    }

    Some of the methods here crosscut database objects. For example I placed the join table for Game Controllers (administrators/organisers) in the Game domain. This can be seen in the AddController and IsController methods on the game.Store interface

    // Store defines the interface for game data access operations
    type Store interface {
    	// Create creates a new game and returns it with the generated ID
    	Create(ctx *cmdcontext.CommandContext, params GameCreateParams) (*Game, error)
    
    	// GetByID retrieves a game by its ID
    	GetByID(ctx *cmdcontext.CommandContext, id int64) (*Game, error)
    
    	// GetByGameCode retrieves a game by its game code
    	GetByGameCode(ctx *cmdcontext.CommandContext, gameCode string) (*Game, error)
    
    	// Update updates a game's settings
    	Update(ctx *cmdcontext.CommandContext, id int64, params GameUpdateParams) (*Game, error)
    
    	// UpdateStatus transitions a game to a new status
    	UpdateStatus(ctx *cmdcontext.CommandContext, id int64, newStatus GameStatus) error
    
    	// AddController adds a user as a game controller (admin)
    	AddController(ctx *cmdcontext.CommandContext, gameID int64, userID uuid.UUID) error
    
    	// IsController checks if a user is a controller for the given game
    	IsController(ctx *cmdcontext.CommandContext, gameID int64, userID uuid.UUID) (bool, error)
    }

    A mock implementation allowed test code to run against an in-memory store. This was a nice feature and easily assembled using Claude. This made unit tests for higher level code fast. Here’s the mock method to update a game status

    // UpdateStatus transitions a game to a new status
    func (m *StoreMock) UpdateStatus(ctx *cmdcontext.CommandContext, id int64, newStatus GameStatus) error {
    	m.mu.Lock()
    	defer m.mu.Unlock()
    
    	game, exists := m.games[id]
    	if !exists {
    		return ErrGameNotFound
    	}
    
    	game.Status = newStatus
    	return nil
    }

    The postgres.go file contained the Postgres implementation. For some reason the CodePro formatting plugin in worpress translates != to its mathematical symbol!

    // UpdateStatus transitions a game to a new status
    func (s *StorePostgres) UpdateStatus(ctx *cmdcontext.CommandContext, id int64, newStatus GameStatus) error {
    	query := `UPDATE games SET status = $1, updated_at = NOW() WHERE id = $2`
    
    	result, err := ctx.DB.ExecContext(ctx.Context(), query, newStatus, id)
    	if err != nil {
    		// Infrastructure error - wrap with context
    		return fmt.Errorf("update game status %d: %w", id, err)
    	}
    
    	rowsAffected, err := result.RowsAffected()
    	if err != nil {
    		// Infrastructure error - wrap with context
    		return fmt.Errorf("check rows affected for game %d: %w", id, err)
    	}
    
    	if rowsAffected == 0 {
    		// Domain error - game not found
    		return ErrGameNotFound
    	}
    
    	return nil
    }

    The stores are stateless and could be singleton. To allow testing a central Stores object was used to hold references to all of the Stores interface. This was passed to the higher level components through constructor injection. Components declared an interface with just the stores that they need. Go’s implicit interface mathcing allowed the large central StoreProvider to satisft it, or the smaller test specific provider. This made testing easy throughout the system. It was even possible to perform quite complex end to end tests with the mock stores.

    There is a lot of boilerplate code in this solution, but AI code generation makes this easy. The risk is inconsistent code. I wrote this using the special offer of free Claude Code tokens in a weekend, at times giving Claude Web instructions on my phone on a bus journey with my family. Production code needs a closer watch. The use of AI generation meant that some subtleties were missed, for example the detection of partial updates in the Update struct. This lead to bugs.

    Attempt 2 – Object Relational Mapping with GORM

    Left with a non-functional system and a lot of AI generated code, I thought I’d start again. I tore out the old domain layer and moved the higher layer logic to a side directory. I left a lot of the framework code intact, but am refining it as I go use case by use case with the care that is really needed.

    The use of GORM can reduce the domain layer to just models. This allowed me to make a flat domain package. Here’s the game structure (or a subset of it) in Gorm. The enums remain the same as before.

    type Game struct {
    	ID                             int64      `gorm:"primaryKey;autoIncrement" json:"id"`
    	Title                          string     `gorm:"type:varchar(255);not null" json:"title"`
    	GameCode                       *string    `gorm:"type:varchar(8);uniqueIndex" json:"game_code,omitempty"`
    	AdminPasswordHash              *string    `gorm:"type:text" json:"-"` // Exclude from JSON
    	Status                         GameStatus `gorm:"type:game_status;not null;default:'creating'" json:"status"`
    	GameType                       GameType   `gorm:"type:game_type;not null;default:'score_attack'" json:"game_type"`
    	JoiningOpensAt                 *time.Time `gorm:"type:timestamptz" json:"joining_opens_at,omitempty"`
      // ......
     	InitialScore                   int        `gorm:"not null;default:0" json:"initial_score"`
      // ......
      
    	// Relationships
    	Creator         *User    `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
    
    	// One-to-many relationships
    	Controllers []GameController `gorm:"foreignKey:GameID" json:"controllers,omitempty"`
    }
    
    // TableName specifies the table name
    func (Game) TableName() string {
    	return "games"
    }

    A nice feature of GORM is that it provides hook functions, so I can always ensure that the Last Modified timestamp is maintained.

    // BeforeUpdate hook to update the updated_at timestamp
    func (g *Game) BeforeUpdate(tx *gorm.DB) error {
    	g.UpdatedAt = time.Now()
    	return nil
    }

    I found myself writing helper functions in the domain layer (in this case in game.go). This is putting strain on the flat domain package structure, so I may at some point break it out again to a package for each domain area. For example I have derived values

    // CanAcceptPlayers a game can accept players once joining is opened up until
    // the game ends.
    func (g *Game) CanAcceptPlayers() bool {
    	return g.Status == GameStatusJoining || g.Status == GameStatusActive
    }

    My helper methods to manage game controllers are now in the game_controller.go domain file, the table that they operate over.

    type AddGameControllerParams struct {
    	GameID       int64
    	UserID       uuid.UUID
    	IsOwner      bool
    	AccessSource AccessSource
    }
    
    func AddGameController(db *gorm.DB, params AddGameControllerParams) (*GameController, error) {
    	gc := &GameController{
    		GameID:       params.GameID,
    		UserID:       params.UserID,
    		IsOwner:      params.IsOwner,
    		AccessSource: params.AccessSource,
    	}
    	return gc, db.Create(gc).Error
    }
    
    // IsUserControllerOfGame checks if a user is a controller (admin) for a specific game
    func IsUserControllerOfGame(db *gorm.DB, gameID int64, userID uuid.UUID) (bool, error) {
    	var count int64
    	err := db.Model(&GameController{}).
    		Where("game_id = ? AND user_id = ?", gameID, userID).
    		Count(&count).Error
    	if err != nil {
    		return false, err
    	}
    	return count > 0, nil
    }

    My business layer uses these and direct GORM methods. This is a snippet of src/commands/admin/new_game.go (coloured differently to denote a different layer).

    	// Create the game
    	newGame := &domain.Game{
    		Title:                    req.Title,
    		GameCode:                 &gameCode,
    		AdminPasswordHash:        &passwordHash,
    		Status:                   domain.GameStatusCreating,
    		GameType:                 domain.GameTypeScoreAttack,
    		InitialScore:             0,
    		JoiningPasscode:          &joiningPasscode,
    		CreatedBy:                *req.UserId,
    	}
    
    	// Use Select to explicitly include InitialScore even though it's zero
    	result := ctx.GormDB().Select("Title", "GameCode", "AdminPasswordHash", "Status", "GameType", "InitialScore", "JoiningPasscode", "CreatedBy").
    		Create(newGame)
    	if result.Error != nil {
    		return nil, fmt.Errorf("create game: create game: %w", result.Error)
    	}
    
    	_, err = domain.AddGameController(ctx.GormDB(), domain.AddGameControllerParams{
    		GameID:       newGame.ID,
    		UserID:       *req.UserId,
    		IsOwner:      true,
    		AccessSource: domain.AccessSourceCreator,
    	})
    	if err != nil {
    		return nil, fmt.Errorf("create game: set owner: %w", err)
    	}
    
    	// Update user's current context to switch them to administering this game
    	err = ctx.GormDB().Model(&domain.User{}).
    		Where("id = ?", req.UserId).
    		Updates(map[string]interface{}{
    			"current_game_id": newGame.ID,
    			"current_context": domain.UserContextAdministering,
    		}).Error
    	if err != nil {
    		return nil, fmt.Errorf("create game: update user context: %w", err)
    	}

    It is here that I found my first subtelty of GORM. It uses the empty value in create and save operations to determine whether or not a field is affected. For this reason if you try to set a team’s score to 0 calling Save it will not work. The code has to explicity request that field be updated, either using Select() or Updates().

    It is possible to set the field to always save, but then it would be far too easy to set a team’s score to 0 by not including the current score in an unrelated update. In fact in a concurrent system it would be dangerous to write things that are not needed. I’ll discuss concurrency below as it’s an interesting topic. Another option is to use a pointer in the structure. This makes the fieled nillable which is conceptually wrong. The end result is this more explicit code.

    I have yet to see how unit tests work in this GORM based system. Claude is writing tests and talks of a GORM testing framework, so I’ll look forward to seeing how this works.

    Concurrent Updates

    Part of my reason to prefer raw SQL was the need to control concurrent updates to team scores. Team score update is an example of a Read-Calculate-Write operation, so faces a race condition if two or more processes attempt it at the same time. A player action can affect the scores of multiple teams, so with multiple players running around town affecting their own and other teams’ scores I had to make this code safe.

    The answer is to take out a database lock. The update code has to be fast so that the lock is not held long. The scope of the lock is the affected teams. This reduces the chance of collision however there is a deadlock risk. If the code locks the player’s own team record then adds in other teams as knock-on effects are calculated then A can lock A then B. At the same time B wants to lock B then A. The system fails if A and B take their own team locks out at the same time, then are waiting on eachother for their second locks. The answer is to work out the set of affected teams at the start then take out locks in ID order. If locking across tables or any other resource, it is important to always use the same order for all processes, so User then Team for example.

    Another type of update I call the Blind Write (possibly based on the term Blind Update in stage lighting). All I care about is that when my transaction commits the player is called Fred. There’s no maths, no reading, just "UPDATE player SET name="Fred" WHERE id=:playerId“. I don’t need a lock in this example. Last to commit wins. For this to work I must only update the fields that I want to set the value of. If I were to read the whole record, make changes, then save the whole record then I introduce a Read-Calculate-Write race condition, even though “Calculate” in this case was “Do nothing”.

    What I have learned so far

    This is a big refactor. It’s going to take time, even with AI tooling (especially now the free credit offer is gone and I’m consrtrained by usage limits). The refactor would have been cheaper if tried sooner. The rush to get a prototype working in about a weekend meant that I had a large system that didn’t work well at all. It also meant that I had something to show the other Scout Leaders that at least demonstrated the idea of what I was making. Refactoring is allowing me to bring back subsystems and functionality slowy, with care, and step by step manual testing. It will be better for it.

    AI does change the landscape by enabling rapid prototyping and iteration. There is an interesting question of how much I should strive for what I see as code perfection versus accepting the AI’s output. What I’m looking at now is not the way I’d have done it, but is it bad enough to demand a rewrite? This is a question I’ve faced so many times working with humans! I tend to ask pointed questions, something I’m continuing with the AI. “Explain this situation…..”. The AI has handled it, but not where I expect.

    AI approach:
    
    * Read Pending Action Fields
    * Are they invalid?
       * **Clear the fields**
    * Is there a pending action?
       * **Clear the fields**
       * Is the user starting a new command?
           * Do normal routing instead
       * else handle pending action
    * else do normal routing
    My Approach:
    
    * Read Pending Action Fields
    * Are they set?
      * **Clear the fields**
    * Is the user starting a new command
      * do the command
    * else-if there's apending action
      * do the action

    I have to say, having typed out the above, I’ll rework it. My way is simpler and always clears down the fields, a fundamental contract in this case.

    It’s expensive for a team leader to ask a junior member to rework something, especially if that work has gone all the way to code review before being checked. I’ve mitigated this in the past by providing support for new developers and having technical planning meetings before starting work. Tools like SpecKit formalise that with AI, and I have my own “SpecKit Lite” on this project. The expense with AI is the token cost, something quite notable with a personal account. Rework is an opportunity cost in either case, but the cost is lower with AI.

    Conclusion – Which Pattern?

    The system will be greatly improved not because of this refactor but because I am now working through slowly implementing features and checking as I go. This is more real coding than the vibe coding of the weekend with Claude Web.

    I think the differences between approaches are marginal. Modern AI tools make creating boilerplate easy so this is no longer a cost. My past use of the raw SQL Store pattern had me using PERL scripts to make the boilerplate which was even more reliable than AI.

    One test will come soon enough when I move short lived data to REDIS. The Store based system would make this easy – use a REDIS backend for those Stores. The refactor may be larger having exposed GORM to my middle tier, but again given AI not too large. The prime candidate is my Memento Store, which already is accessed via CreateMemento and GetMemento methods.

    Later Updates In This Series

    Writing the Party-Bot – in which I implement a Party Planner Bot very quickly on top of the game framework. (A Party is a new type of Game in which people organise a party!). I come more to the realisation that Store Pattern made sense!

  • Using Go To Call Salesforce with the JWT Bearer OAuth Flow

    When using Go to call Salesforce’s OAuth with the JWT Bearer Flow you may see hard to resolve authentication errors if you are using the jtw.RegisteredClaims struct to build your JWT claims. We found that Salesforce requires the Audience claim to be a single string, for example "aud": "https://login.salesforce.com". The jwt package defaults to sending Registered Claims as an array of strings.

    This sample code shows how RegisteredClaims may be used to build a JWT. This JWT will fail authentication.

    claims := jwt.RegisteredClaims{
      Audience:  jwt.ClaimStrings{"https://login.salesforce.com"},
      Issuer:    "your-client-id",
      Subject:   "user@example.com",
      ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) // FAILS
    

    There are two neat solutions for this problem (and one very un-neat solution which Claude tried, and succeeded, when we encountered this). A simple solution is to use jwt.MapClaims. This solution lacks the type safety of RegisteredClaims

    claims := jwt.MapClaims{
      "aud": "https://login.salesforce.com",
      "iss": "your-client-id",
      "sub": "user@example.com",
      "exp": time.Now().Add(time.Hour).Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    

    The RegisteredClaims package provides a global variable which, when set to false, will cause the library to marshal any single entry jwt.ClaimStrings as a string. This is a global setting, so lacks fine grain control. It worked in this situation because we only have one ClaimStrings.

    jwt.MarshalSingleStringAsArray = false
    
    claims := jwt.RegisteredClaims{
      Audience:  jwt.ClaimStrings{"https://login.salesforce.com"},
      Issuer:    "your-client-id",
      Subject:   "user@example.com",
      ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    

    Claude’s solution? Claude wrote a custom RegisteredClaims implementation which satisfied the required interfaces. This is longer code and works, but I wonder how maintainable it is especially as the library evolves.

    There are two security practices that I believe are important if you are doing this type of work:

    • Restrict the host names that you will send tokens to in order to login. If you are writing this as bespoke work and only have one org or always use login.salesforce.com and test.salesforce.com as your login domains then this is easy. Just hard-code these and provide a configuration switch between sandbox and production.
    • Trust the instance URL returned by Salesforce in their token response. This guarantees that API requests cannot be sent to the wrong host.

    Remember also that the JWT signing key and the access tokens that you receive on successful authentication are highly sensitive information. Compromise of either could be devastating.

    Resources