← StateDB development log

StateDB: Finishing the reorganization and introducing a repository

Finishing the reorganization, introducing a repository, and letting the program continue after a failed item lookup.

I finished the reorganization and got the functions working. In the previous post, I had started moving connection setup and queries out of main. This time, I also introduced a repository struct.

Giving the repository the database pool

Previously, a call looked like this:

itemFound, err := testitem.GetByID(ctx, db, 4)

I was passing the database pool into the function each time. Now the testitem package has a Repository that holds it:

type Repository struct {
	db *pgxpool.Pool
}

func NewRepository(db *pgxpool.Pool) *Repository {
	return &Repository{
		db: db,
	}
}

The db field holds a pointer to the existing pool. NewRepository creates a repository with that pointer and returns a pointer to the repository itself. It doesn’t open another pool.

In db: db, the name on the left is the struct field, and the name on the right is the function parameter. This is where the pool gets assigned to the repository.

Right now, the repository only holds the database pool, but it gives me somewhere to add dependencies later. I can provide them when I create the repository, and its methods can use them without me passing everything into every call.

Calling it from main

After setting up the database pool, I can create the repository once:

repo := testitem.NewRepository(db)

Then the call becomes:

itemFound, err := repo.GetByID(ctx, 4)
if err != nil {
	log.Println(err)
} else {
	fmt.Printf(
		"ID: %d, Name: %s, Status: %s, Created At: %v\n",
		itemFound.ID,
		itemFound.Name,
		itemFound.Status,
		itemFound.CreatedAt,
	)
}

GetByID is now called as a method on repo. The repository holds the pool, so I only pass the context and the ID for this operation.

The error check and printing still happen in main. The difference is how the query code gets access to the database.

A missing item shouldn’t stop everything

I also changed how I handled errors when getting an item. Before, the error branch used:

log.Fatal(err)

That stopped the whole program, even when the problem was simply that the item wasn’t found. I changed it to:

log.Println(err)

Now it logs the error and continues after the if/else. The item details only print in the else branch, when the lookup succeeds.

This change applies to any error returned by GetByID, not just a missing item. It doesn’t distinguish between “not found” and another database error; it changes whether that error ends the program.

For this step, I wanted a missing item to be something the program could report and move past.

Removing unnecessary else branches

I also got rid of unnecessary else statements where the error branch already stops the program. For example, with log.Fatal, I had code shaped like this:

if err != nil {
	log.Fatal(err)
} else {
	fmt.Printf(
		"ID: %d, Name: %s, Status: %s, Created At: %v\n",
		itemFound.ID,
		itemFound.Name,
		itemFound.Status,
		itemFound.CreatedAt,
	)
}

If it hits log.Fatal, it won’t continue anyway. So the same behavior can be written without the else:

if err != nil {
	log.Fatal(err)
}

fmt.Printf(
	"ID: %d, Name: %s, Status: %s, Created At: %v\n",
	itemFound.ID,
	itemFound.Name,
	itemFound.Status,
	itemFound.CreatedAt,
)

That removes a level of nesting without changing what happens.

This is different from the log.Println example above. Logging alone doesn’t stop execution, so moving the print outside that else would print item details even after a failed lookup. There, I kept the success path separate.

Another piece coming together

The earlier work on structs, pointers, and functions is starting to connect. Here, the struct holds something the query methods need, and main can call those methods through the repository.

The reorganization is finished, the functions work, and this gives me a clearer place to keep the test item operations as I continue building StateDB.