Category: Uncategorized

  • The Return of the Stores

    .. in which I go almost full circle (or spiral upwards perhaps) in the implementation of my Go based WhatsApp Wide Game Bot

    Back in the article Three Tier Archirecture I discussed how I was moving away from a Stores based architecture for my database access layer towards just using GORM. I was concerned about the amount of boilerplate SQL code that was being generated (admittedly largely by AI) so thought that an Object Relational Manager could solve this for me. I’ve used Hibernate in the past. Maybe I could just write my business logic to work over GORM.

    The business layer uses a Command Pattern. This allows control of transaction lifecycle to be taken out of the business logic, and allows business logic elements to be composited as needed. A Command can call a Subcommand and inherit its transaction scope, with the Commands completely unaware of how transactions are managed. It’s a great separation of concerns.

    Go’s packages are large flat namespaces in their own rights. My flattened domain package was steadily being cluttered my methods like GetOrCreateUser(), though perhaps this should have been a Command. Also I want to move some of my domain into REDIS, for example the Memento System for short term memory in the system. Mementos handle questions like “If the user pressed WhatsApp button with ID abcde-1234-abcdef when was this generated and what does it mean?” My solution has achieved three things:

    • I now have Store classes which group methods by domain type, for example domain.UserStore.GetOrCreateUser().
    • These methods are Command Methods in my Command Pattern framework, so they composit into business logic commands naturally.
    • I can easily swap my Memento Store for a REDIS Memento Store. I can also start using REDIS caching in my User Store if I need to, but this system is not struggling to scale at the moment!

    Start From The Top – The Command Pattern for Business Methods

    Traditionally Command was an object (or interface) and Command Pattern in the Gang Of Four Design Patterns is more about storing what was done and being able to replay or undo commands. (Some may be thinking of Event Sourcing Frameworks here too). The Command Interface in this system has a method, Execute() which takes a Command Context that holds the database transaction and any other data I add. In this case I add user information that is frequently referred to, allowing me to read the user record once at the start of the incoming request handler.

    When using an interface for Command, we have a Command Runner that accepts the Command, creates the transaction, calls the Command, then commits or rolls back as appropriate. The Command instance is set up with the data it needs and, after completion, holds any result.

    Modern languages support lambdas, the ability to pass a function along with captured state. C had function pointers. Lamdas add the ability to enclose values as well. It’s very powerful, and allows for a Command in this pattern to be a Lambda.

    The pattern to call a business logic command is the same as that to call a Store Method under this new pattern. Here’s a call to business logic to either switch the current user into an admin context or create a new user as admin for a game. It handles the “/admin” command in my widegame bot

    Go
    result, err := cmdcontext.Run(request.BaseRequest, gamecmd.AdminSwitch,
      gamecmd.AdminSwitchRequest{
    		Caller:        caller,
    		User:          request.UserContext(),
    		GameCode:      gameCode,
    		AdminPassword: password,
    	})

    BaseRequest acts as a context for the whole call. It holds the Go Context as well as a request ID for logging and the framework Executor instance which holds the database connection pool. gamecmd.AdminSwitch is the function to call. The single argument is a request object and the result is returned alongside error following normal Go patterns.

    I’d have liked Run to be a method on Executor, but Go Generics do not support methods. It has to be a static function call.

    The signature of gamecmd.AdminSwitch is

    Go
    func AdminSwitch(ctx *cmdcontext.CommandContext, req AdminSwitchRequest) (*AdminSwitchResult, error) {

    CommandContext provides the current database transaction, and soon to be REDIS connection for upcoming REDIS stores.

    A Command can composite another Command by calling it directly, passing down its CommandContext. It can similarly call a store method (which is just another Command) the same way. I’ve migrated the Party Web Server so will show an example of calling a Store method directly from the top layer. This jumping of layers from presentation to domain is fine here – saves me writing mid-tier Commands that just delegate straight to a domain method. A Party is a type of Game in a system that was created to play urban wide-games.

    Go
    	// h is the HTTP handler instance for the Party Web API server
    	// h.gameStore() is a convenience method to read h.storeProvider.GameStore()
    	// h.storeProvider is an interface that is a subset of the central StoreProvider.
    	// partyapi.StoreProvider declares the stores required by the Party API Server.
    	// Go's interface matching allows me to do this. I can unit test the handler with
    	// the smaller subset of mock stores that it needs.
    	game, err := cmdcontext.Run(req.BaseRequest, h.gameStore().GetByGameCode, gameCode)
    	

    A Tradeoff

    If I have a store per domain object, then I load each store one at a time. For example in the Party Bot I may

    Go
    // pseudocode
    party := PartyStore.getPartyById(partyId)
    food := FoodStore.getPartyFoodByPartyId(partyId)

    If I allow GORM into higher layers then I can join or use subselectes. Maybe the optimisation is to provide these joining methods in the stores. In which case does PartyStore also provide the methods for the food? After all, food is part of a good party! Where do we draw our lines?

    Go
    // pseudocode
    partyWithFood := party.Store.getByIdWithFood(partyId)

    If Party was not just some small special case of Game (or if this system grows into the next Mega-Online-AI-Powered-Party-Planner) then Food may become its own little domain area with so many other methods.

  • Claude likes to talk about what’s been, not what is

    Consider this diff suggested by Claude Code:

    Diff
    +    // Call OpenAI using new Chat interface
    +    response, err := client.Chat(
           request.Ctx,
    -      systemPrompt,
    -      request.Message.Content,
    -      tools,
    -      executors,
    +      ai.WithSystemMessage(systemPrompt),
    +      ai.WithUserMessage(request.Message.Content),
    +      ai.WithTools(tools, executors),
    +      ai.WithToolActionLogCallback(logCallback),
         )

    Notice the comment “Call OpenAI using the new Chat interface”.

    Once I delete the old Chat interface then the new Chat interface will just be The Chat Interface. There will be no old one to consider. This just calls OpenAI. That’s it.

    I guess I need to update CLAUDE.md to tell it to only ever comment on what is, not what has been. Comments should describe what happens now and why.

    The callbacks here are to provide a list to the user of actions taken by the tools themselves. I was finding I did not trust the AI to have done what it said (although recent manual testing showed it was doing well). If the system reports the actual change made then I as a user can have confidence that my command was correctly actioned.