Go + Gin API Implementation: Clean Architecture, JWT, Redis, and Transaction Management


September 24, 2025
Program

Go + Gin API Implementation: Clean Architecture, JWT, Redis, and Transaction Management


Experience sharing on Go + Gin API implementation, covering Clean Architecture layered design, CRUD, JWT, Redis caching, transaction management, and comparing development differences between C# and Go, Swagger documentation management, and common pitfalls. Suitable for backend engineers.

Introduction

🔗

I mainly use C# and Node.js for backend development. Recently, I noticed a significant increase in Go job openings, spanning industries like fintech and Web3. These are high-traffic, profitable sectors. After some research, I found that Go offers better performance and simpler syntax for microservices and high concurrency scenarios, which motivated me to learn Go.

As usual, when exploring a new language or framework, I build a sample project to quickly implement various features from scratch if needed, helping speed up development.

This article uses Go and Gin to build a basic backend API template, with a layered structure, implementing CRUD, JWT authentication, Redis caching (with auto-reconnect), and transaction management—covering common backend features.

GitHub Source Code

Project Architecture

🔗

The project adopts a layered architecture, dividing the system into four layers: Handler for handling HTTP requests, Service for business logic, Repository for database interactions, and Model for defining data structures.

This makes the code easier to maintain and test, with clear separation of responsibilities. For those familiar with C# or frameworks like Nest.js, it's easy to map the logic into request handling, business logic, data access, and data definition.

Pitfalls Encountered

🔗

Swagger + Model Alias Conflicts

🔗

When using Go with Swagger (swaggo/swag) to generate documentation, it was a bit confusing at first because API annotations must be written directly above the Handler function, and even the routes are defined there. When setting response or request formats, you can directly reference already defined objects, but these objects often come from different model packages.

Here's the issue: if you need to import structs from both user/model and order/model, in Go you can use import aliases (userModel.User, orderModel.User) to distinguish them. However, Swagger completely ignores aliases when generating documentation and only recognizes the struct name, which leads to conflicts.

My current solution is to centralize potentially shared structs in a separate commonmodel folder. This way, when importing modules, you can effectively distinguish between model and commonmodel, avoiding conflicts.

The community's suggestion is to add //@name to the struct, giving it a Swagger-specific name (such as UserModel, OrderUserModel) so the documentation can differentiate them. Since my business structure isn't too complex, using the commonmodel approach is sufficient for now, so I haven't tried //@name yet.

At least this ensures the Swagger documentation remains clean and conflict-free.

Value vs Pointer

🔗

When transitioning to Go, value (*) and pointer (&) concepts are the most important things to adapt to.

In languages like C#, Python, and Node.js, most objects are reference types by default. Modifying properties usually affects the same data instance, so you rarely need to think about "pass by value or by reference."

Go is different: structs are value types—passing or calling methods on them copies the data. If you want to modify the original content, you must use a pointer (*T). This directly affects the design of method receivers:

  • Value Receiver: Calling the method copies the data; modifications inside the method do not affect the original object.
  • Pointer Receiver: Passing a reference; modifications inside the method directly affect the original object.

For those coming from C# or dynamic languages, this is the first common pitfall. In other languages, you rarely need to worry about these details, but in Go, you must clearly decide "whether you want this method to modify the original data." This is part of Go's emphasis on explicitness and clarity in design.

For more details on value (*) and pointer (&): ➡️ Go Pointers Explained: A Beginner’s Guide to & and *

Transaction and Clean Architecture

🔗

In this project, I aim to maintain the principles of Clean Architecture:

  • Repository: Responsible solely for DB CRUD operations.
  • Service: Focused on handling business logic.

However, when business processes require multiple repositories to work together (for example, creating a user while also creating an order), the question arises: where should transactions be implemented?

If each repository manages its own transactions, things quickly become chaotic—transaction logic gets scattered everywhere, making maintenance and testing difficult.

Therefore, I chose to implement an additional Unit of Work and import it at the Service layer, wrapping multiple repository operations into a single transaction. This ensures data consistency and keeps responsibilities clear: Repository focuses on CRUD, Service controls the transaction scope, and the overall architecture remains clean, manageable, and true to Clean Architecture.

Future Plans

🔗

I originally wanted to implement other features like high concurrency, parallel processing, and message queues in this project, but later realized it would make the project overly complex and make it harder to find or reference core logic examples.

So I decided to leave these advanced topics for future projects, allowing each project to focus on a single theme. This should make learning, maintenance, and example referencing much simpler.

Conclusion

🔗

Compared to ecosystems like C# or Nest.js with comprehensive frameworks, Go lacks many "out-of-the-box tools.

  • Pros: Highly flexible, excellent performance.
  • Cons: You need to define many structures yourself; without proper planning, projects can easily become chaotic.

However, Go's biggest highlight is its lightning-fast startup speed, making it perfect for microservices—single-responsibility services with simple structures can run lightweight and efficiently. This is the most impressive aspect I've experienced.

Honestly, I wasn't very interested in Go at first. The lack of all-in-one frameworks means engineers need to assemble packages themselves, which brings flexibility but can be chaotic without a solid foundation. But when I saw the Go mascot, I thought, "So cute, I have to learn it! 🫠"

GoSystem DesignWeb



Avatar

Alvin

Software engineer who dislikes pointless busyness, enjoys solving problems with logic, and strives to find balance between the blind pursuit of achievements and a relaxed lifestyle.

Related Posts