Category: Software Engineering

  • 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.

  • JetBrains Junie AI for Go

    I’m impressed. It seems to understand Go better than Claude, so move over Claude! I just wonder how much use I have on my personal license.

    To see how it’s handled writing unit tests look at this commit.

    https://github.com/m0rjc/goconfig/pull/17/changes/eb7c04fa90b9dd47f32b30dddf9b3775c04c9c25

    This was achieved with a fairly simple prompt. I’ve made some manual changes, added some more cases to the tests, but the structure it’s used is easy to work with. My initial prompt was

    Markdown
    Can you create unit tests in the process package which, for each type we handle, call the process.New method and run the resulting process. (So you'll need a StructField to apply reflection on). We're interested for each one that we prove we can read a valid rawValue into the type, that we handle invalid input (like reading 'foo' as an integer), and that the validators work (min, max, pattern). Order them with the _types.go file so number_types_test.go

    It’s nice to work with a proper IDE, unlike VSCode for this. I can run and debug individual test cases include parameterised test cases:

    Looking at a test file with buttons I can press to run or debug inidivual tests

    I have one click access to coverage with the normal highlights in code, which has helped me to ensure that I am testing all cases.

    Code coverage shown in the left margin. I’m not testing the system with a custom parser and no default handler

    The test that I need to complete this coverage was largely written using the AI code assistant. I had to help it along a little, but it generally got the idea.

    New test for a custom type parser, showing the run test dropdown

    One more test to write to bring the coverage up. Clearly I need to learn to start a new task in Junie as it still shows me as working on its initial setup task.

    Junie user interface adding a unit test

    Now I’ve tested the process package I can have Junie work on the main package. I’ve deleted all of Claude’s tests and asked Junie to test the functionality of the system.

    A nice thing is that it’s found a missing feature, a means of using this tool that I hadn’t envisaged, and put in a fix. I wonder how easy it will be to have the system not instantiate a nested struct if no keys for that struct are available or some other decision. This would be a future story.

    Junie amending config.go so that it can handle nested pointers to structs

    It’s not all bad for Claude

    Junie is very much an assistant. I’ve not tried large tasks with it yet, but it helps me very nicely.

    I can’t see how to interact with Junie while it is running. There doesn’t seem to be away to look at a proposed change and stop the process there with instructions to correct. I can hit the big stop button, but not review as I go. Maybe there’s a button I need to find to enable this. It’s not been a problem for small tasks.

  • 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.

  • Working on an open source module for a closed source system

    My large person project is the Wide Game Bot. This is a pretty complex system involving WhatsApp, OpenAI and Kubernetes, Postgres, REDIS and Cloudflare currently sitting on a DELL 7070 Micro in my home office. It has already successfuly branched out into a different kind of wide-game, the Scouts Christmas Party Planning Wide Game, and work on our location based urban wide game continues.

    The system has two components which I wish to open source. This is as much as a demonstration of my abilities without revealing my main intellectual property, the game system. There is also the hope with all open source that these things become useful in their own right, and maybe they can grow more as open source than I will achieve on my own.

    Go AI Tools

    Go Ai Tools is at https://github.com/m0rjc/goaitools. This provides connection to OpenAI, a tool calling Agent loop and support for message state tracking.

    Go WhatsApp

    Go WhatsApp is not currently released, although it exists as an independent module in my codebase. I use a redirect in the go.mod file to allow local development. It currently does not support egress rate limiting and does not understand message receipts. These are critical requirements for a production system, and without them I don’t believe I can release this code to the public.

    Switching Hats – The difference between two worlds

    My work on the bot is quite experimental really. I’ve built it up, torn it down, thrown Claude Code at it. I’m working with a system with flaws that I’ll get to fixing as I go. It’s very much an early days research style project which is allowing me to rapidly iterate, test and come back and reiterate.

    My work on goaitools is like wearing a different hat. Even though it’s still a pre-release version (currently v0.3.0) I’m working to Stories. I’m ensuring that tests are in place at every step, something that’s already served to reign in Claude when it gets things wrong. I’ve tried to make the tests behavioural and wonder if I can tell Claude in its CLAUDE.md that it should always be this way. There is documentation. There are samples, which are also system tests. There is a backlog and release plan.

    AI tools make large changes relatively cheap, but I am trying to build goaitools properly, with isolated components and separation of concerns divided through small interfaces. This is more how I have worked in my day job. It reall feels different to the wide game bot.

    Which is better?

    It’s a good question. Wide-Game-Bot can evolve rapidly. It accepts a mess. One day I’ll make those hard component boundaries (before it becomes too messy) but I’m heading there incrementally. GoAiTools feels slower because I’m being more thorough. When I wear my goaitools hat I’m no longer the bot developer. My interest is in that library and my other self is very much on the other side of its boundaries. I don’t compromise GoAiTools for the bot project.

    Claude and large intertwined codebases

    I may have to compartmentalise the wide game bot sooner rather than later, as Claude struggles to cross compartments. This is resulting in a lot of “stabbing in the dark” as it tries to make changes.

    Claude Code screenshot showing Claude changing its mind about a variable declaration.
    An example of Claude stabbing in the dark on the AI Bot project.

    I think things like this would be reduced if the codebase was made of smaller components with well defined boundaries. The components exist in my head, but rapid work with Claude has been less disciplined. An issue is managing dependencies.

    I’ve seen the same with human teams working under pressure and it comes back to the same question about developer discipline and maintaining structure in a project. Why does processor.go know about conversation TTL anyway? Its job is to route requests to the right agent, not to be the agent itself. Claude, like a human team, degrades if the system is allowed to become messy.

    Unit Tests

    I’ve seen Claude correct itself quickly due to failing unit tests in GoAiTools. At least component tests, verifying the contracts between system parts, allow errors to be spotted sooner.

    So which is better?

    So to answer the question of which is better – the stricter work on GoAiTools may feel slower, but I think in the end it wins out for the same reason that maintaining developer discipline and looking after tests wins out in traditional projects.

  • Working with AI

    Working with AI

    I’ve been working with multiple AI tools both professionally and in my own time for a while now. At home I have a personal Claude Pro account and use Gemini on the web as well as my own OpenAI developer account. I have used OpenAI to help with tasks (“You are an expert in planning activities for Scout Groups....“) and programatically in projects like the WhatsApp Party-Bot. Gemini does research for me and can help with code snippets and understanding, somtimes giving a second opinion to Claude. Claude is my main code assistant.

    It’s certainly helped with rapid prototyping and iteration on a project. I was able to work on the Party-Bot, adding features and fixing things on a live system as I received user feedback. Party-Bot now has a traditional web page too, for those who don’t like the conversational UI. This was all built in hours.

    Some of my working patterns

    My employer uses SpecKit. This reminds me of the old days before Agile when we designed and specified projects! We turned a project around this way. My first project with Polk was a restart on a failing project. We spent weeks planning it. I developed my reputation as “A Danger With Whiteboards” due to the amount of UML that I drew on them. Management worried about time spent without any code being written, but when we wrote the code we delivered a fully functioning system in less time than the failed project had wasted. The system was also robust to change as customer requirements came in.

    Agile has always been “mini waterfalls”. I like to point to pictures of Aysgarth Falls in the Yorkshire Dales. In my time leading the Foundations Team in FinancialForce (now Certinia) I’d spend the first half day or day of a sprint with those whiteboards. We’d work on specifying exactly what we were going to implement.

    A beauty of this is that any questions that arose could be dealt with before we all went away and worked. Everybody knew what they were doing. I’d insist on what I consider to be “mathematically correct systems” in which we knew what the definition of the system was. Some Agile purists disagreed, wanting strict focus on the User Story at hand. We could design the system with clear contracts allowing differerent developers to split work and come together later – and it all worked in the end!

    My WhatsApp Game Bot framework has reached the stage where different teams could go off and implement different subsystems all independently now – but there’s only one of me on this! I’ve been here before. “You need a team. You don’t scale on your own”. The bot can only be split because I have driven Claude to write a modular architecture. If you let it make spaghetti then you cannot factor out parts this way.

    Mini-Waterfalls with Claude

    Claude tends to create a SpecKit like Story structure, even without SpecKit installed. I’d have it work with me on the research and specification, using Planning Mode to help me work out and write the story. I’d then write the story file and write a planning and task list.

    Writing out tasks lists allows me to start afresh in a new Claude Context. Context costs, because we’re paying for tokens. It also can slow things down, and I’ve found that if you give Claude too much information it’s more likely to go wrong. Claude has to be told to tick off tasks as it does them, or it forgets. Keeping the task list up to date allows for better recovery should the session be lost for any reason.

    I switch between policing every change that Claude offers and letting it rip with a review at the end. A lot depends on how confident I am with the specification. Policing every change allows me to trigger a change in direction if I see something I disagree with. This saves a big refactor later. Letting it rip could mean not having the work quite as a I’d do it, but having something working then deciding whether to refactor or not. I’ve not had Claude go wildly wrong. It’s easier to delete a broken AI generated branch in Git than it is to tell a team member a day before the end of a Sprint to rework everything!

    Habits to Change

    This is how I work with humans. I ask a question even though I know the answer. With humans I may keep asking to get to the point. I want the person I’m helping to work it out so that they understand.

    I ask Claude "Looking at the GameType and UserContex I
think GameType can be a string throughout the system as it's registry based now, so we lose the
enum from the domain layer too. UserContext isn't going to grow though. We could marshal back and
 forth, or do we define it lower down in cmdcontext and refer to it from domain?"

Claude answers
"Good architectural question! Let me explore how these types are currently used to make an informed recommendation."
    Asking Claude questions, not just giving it commands

    This wastes tokens when I’m working with an LLM. I know what I want. I should just demand it. In this case Claude came to the same conclusion that I’d already reached. This was to correct a decision that Claude had made earlier as part of a large refactor, in which it was marshalling enums across the layers. You can’t have switch statements on Game Type buried in multiple parts of the code when all that matters is that Game Type is read from the database and used to find the correct Game Strategy based on name.

    Like hitting a golf ball down a course, if you’re not Tiger Woods

    The AI isn’t perfect. It doesn’t do things the way I would. But then am I perfect? Definitely not! Development of any system is iterative. I like to think of it like hitting a golf ball down the range. I’m not particularly good at aiming golf balls so I’ll hit off to the side a few degrees or tens of degrees, but the aim is roughly right and I’m moving forward at every stage. I then need to go find the ball and hit it again trying to correct.

    The Dynamics of Refactoring

    One of the fundamental promises with Agile Development is that we can refactor. The idea is that we must spend time collecting the technical debt that we accrue. Ideally we should allocate time to keeping on top of this and maintaining the system. This is hard to do in a business environment that is always under pressure to deliver. The business wants features. Refactoring is not feature delivery.

    Not refactoring is false economy. The system degrades such that working on it becomes slower and more expensive. A good system with good separation of concerns, sensible dependencies, SOLID design is easier to work on. I’m trying to keep the Game-Bot this way because time spent making that foundation allows for faster iteration down the line. If the foundation is good then all I need to do is write the new feature business logic or the new Web presentation layer. I think Party-Bot is going to become primarily a web interface built on top of the same Domain and Business layer as its WhatsApp incarnation.

    As Foundations Team leader I kept “Richard’s Red Refactor List” of things I wanted done to maintain the system. We allocated time, sometimes a whole sprint, to clear that list. This kept a system that was easier to work on. Nowadays we have a Technical Backlog in JIRA and pull technical stories into the sprint as needed.

    Claude makes refactoring cheaper than it was. Will that still be the case when Claude has made delivering features cheap? I think this will level out. In a team situation we’ll still be potentially disrupting work for a large refactor, so the task of coordinating in a team remains. Claude does remove a lot of the grunt-work – the rippling of change through the codebase if you change a low level interface that everything depends on. (Back in my Java days I found Eclipse’s refactor tools were great for this. I’ve not seen anything comparable in any other IDE since.)

    Currently, for this sole developer, refactoring is cheap – or relatively cheap. There’s still an Opporunity Cost…

    The Claude Rate Limit Options dialog. What do I want to do? Stop and wait or pay for more access?
    The Claude Rate Limit Dialog – Stop and wait or pay for more?

    The plight of the Junior Developer?

    £18 per month for Claude Pro is significantly cheaper than a junior developer! This cannot be denied and its impact on the industry is going to be profound.

    Claude still needs guidance. My job remains safe – for now. Perhaps it always will, or will for long enough for me to reach retirement, because a fundamental need in software engineering is to nail down requirements. User wants are often fuzzy. Computers, even Claude, need definite rules to determine what the system must do. They need that “system definition”.

    So what happens when all the seniors retire? Who will take our place? Who will have learned the ropes and gained the experience? Who will be able to tell Claude it’s made a massive security hole or be able to drive it towards laying out that scalable system with its well designed components?

    Maybe AI is just the next Industrial Revolution or Communications Revolution. I lived through the 90s and saw the last one. We take the web for granted now – it’s such an invisible part that underpins so much that a UK political leader once pondered whether the state should be as responsible for ensuring access as it currently is for roads.

    Maybe the next junior developer will be someone entering the workplace ready to use the new tools, as a past engineer may have entered the workplace ready to use a CNC machine tool (or MATLAB for the kind of stuff I studied). AI is an automation. Improvements in tooling for the physical world have increased scale. Consider the robotic warehouses we see now. But automation also impacts jobs.

    I think the biggest question is whether demand will keep up with increase in supply, as software engineers become capable of doing more in less time.

  • 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.

  • My Bot is connected to WhatsApp

    My Bot is connected to WhatsApp

    That was a lot harder than it should be? Or are WhatsApp trying to make a barrier to entry to ward off amateur developers?

    Generate a token to use

    You’ll need a System User with permissions to access your app and the WhatsApp account you are trying to make work.

    I used an admin user which is distinct from the employee user that my app uses. This admin user is given a token for as long as I need it, then revoked immediately that I have finished. I don’t like admin users floating around, but can’t delete it. Revoking its token should be enough.

    Activate the number once it has been set up

    One you’ve registered your number using “Add Number” in the API setup page it will show as “Pending” in Whatsapp Manager. You need to activate it. This is by POST request.

    The PHONE_ID is the phone number ID which you can find in the dropdown in the API setup page where it offers to make CURL requests for you.

    PIN is a two factor PIN that you are creating with this call. It musty be 6 digits. I don’t know if you need to remember it (you can reset it later from the UI). I generated a 6 digit random number.

    Bash
    curl "https://graph.facebook.com/v21.0/$PHONE_ID/register" \
            -H 'Content-Type: application/json'\
            -H "Authorization: Bearer $TOKEN"\
            -d "{ \"messaging_product\": \"whatsapp\", \"pin\": \"$PIN\" }"

    You can check this has worked by performing a GET request. You should see that it is a business number with a webhook, but the webhook won’t work yet.

    JSON
    {
      "verified_name":"The Name You Provided When You Registered",
      "code_verification_status":"VERIFIED",
      "display_phone_number":"--REDACTED--",
      "quality_rating":"GREEN",
      "platform_type":"CLOUD_API",
      "throughput":{"level":"STANDARD"},
      "webhook_configuration":{
        "application":"https:\/\/--REDACTED--"}, 
      "id":"--REDACTED--"
    }

    I’ve set my number to be non-searchable. I don’t want random people contacting it.

    Bash
    curl "https://graph.facebook.com/v21.0/$PHONE_ID/" \
            -H "Authorization: Bearer $TOKEN"\
            -d '{"search_visibility":"NON_VISIBLE"}'

    Subscribe its WhatsApp Account to my App

    This is not enough to allow webhooks. You can use the API to set up the webhooks. Mine is set up in the App Dashboard and that seems enough.

    Find the phone number’s WhatsApp account and read its ID. This is in the Business Suite under WhatsApp Accounts and the ID is near the top.

    A GET request lists the subscriptions. All you need to do is POST with your token.

    Bash
    curl -X POST "https://graph.facebook.com/v21.0/$ACCOUNT_ID/subscribed_apps" \
            -H "Authorization: Bearer $TOKEN"\

    You can then check it has worked with a GET request to the same endpoint.

    References

    Start here:

    https://developers.facebook.com/docs/graph-api/reference/whats-app-business-account/phone_numbers

  • 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!