DEV Community

Adexandria
Adexandria

Posted on

Exploring Go through backend engineering practices (Part 1)

During my first week working with Golang, I focused less on basic syntax and more on applying familiar backend engineering principles within a new ecosystem. The goal wasn’t just to learn Go, but to understand how established patterns translate into its design philosophy.

To explore this, I built a simple to-do list API using the Gin framework, SQLite for persistence, and Uber’s Dig for dependency injection.


Project Architecture

The application follows a layered architecture to maintain separation of concerns:

  • Handlers: manage HTTP requests and responses
  • Services: contain business logic and orchestrate operations
  • Repositories: handle data access and database interactions
  • Models: define domain entities and DTOs

This structure ensures that each layer has a clear responsibility, making the system easier to maintain and extend.


Database Integration

The first step was establishing a connection to SQLite using GORM. This provides a convenient abstraction over SQL while still allowing control when needed.

func connect() *gorm.DB {
    db, err := gorm.Open(sqlite.Open("todo.db"), &gorm.Config{})
    if err != nil {
        panic(err)
    }

    err = db.AutoMigrate(&models.Task{})
    if err != nil {
        panic("failed to connect database")
    }
    return db
}
Enter fullscreen mode Exit fullscreen mode

Dependency Injection in Go

Unlike frameworks that rely heavily on implicit dependency wiring, Go encourages explicit composition. To manage dependencies cleanly, I used Dig to construct a container for all registered components.

Instead of instantiating dependencies directly in main, constructors are registered and resolved through the container.

// Registers db dependency used to connect to the SQLite
func RegisterDb() *dig.Container {
    container := dig.New()

    err := container.Provide(connect)
    if err != nil {
        panic(err)
    }

    return container
}
Enter fullscreen mode Exit fullscreen mode

Repository Pattern and Abstraction

To decouple business logic from data access, I implemented the repository pattern. By depending on interfaces rather than concrete implementations, the application remains flexible to future changes (e.g., switching implementation of a behaviour).
Concrete implementations are then registered with the DI container as the interface type, allowing seamless resolution across layers.


Application Bootstrap

Finally, the application is wired together in main. The DI container is built, handlers are registered, routes are initialized, and the server is exposed on port 8080

func main() {
    container := services.CreateContainer()

    err := container.Provide(handlers.TaskHandler)
    if err != nil {
        panic(err)
    }

    router := gin.Default()

    err = container.Invoke(func(h *handlers.Handler) {
        publicRoutes := router.Group("/api/tasks")

        publicRoutes.GET("/:id", h.GetTaskById)
        publicRoutes.POST("/create", h.CreateTask)
        publicRoutes.PUT("/:id", h.UpdateTask)
        publicRoutes.DELETE("/:id", h.DeleteTask)
        publicRoutes.GET("/", h.GetAllTasks)
        publicRoutes.GET("/filter", h.FilterTasks)
        publicRoutes.GET("/search", h.SearchByTask)
    })

    if err != nil {
        panic(err)
    }

    err = router.Run(":8080")
    if err != nil {
        return
    }

}

Enter fullscreen mode Exit fullscreen mode

Final Thoughts
Working with Go has been less about learning new concepts and more about rethinking how familiar patterns are applied in a language that prioritizes simplicity and explicitness. While the ecosystem differs from more opinionated frameworks, the core engineering principles remain consistent.

Next, I’ll focus on strengthening observability with structured logging and implementing more robust error handling.

View the github repo

Top comments (0)