Things I miss about Spring Boot after switching to Go

I wrote production systems for a startup using Java and Spring Boot for 1.5 years before switching to Go. It was a fast-paced startup, so we were shipping features every week. I got hands-on experience with backend technologies such as Java, Spring Boot, MySQL, Redis, and Kafka.

Recently I made a switch to Go because I wanted to move forward from just writing APIs & microservices. Go has been fantastic to work with, but moving away from the Spring ecosystem made me appreciate several things Spring Boot gets right.

💡 I’m not writing this post to start a war between Spring Boot and Go developers. This post is about my experience. Both ecosystems have their own strengths and use cases, so I’m sharing what I personally miss from Spring Boot.

A Complete System

Spring Boot is a complete system for backend stuff. It is designed with the “Batteries Included” philosophy, so you get most of the things out of the box. You will get most of the production features out of this box.

Go does not have a framework with the same scope as Spring Boot. This is not because the Go ecosystem is immature or Go is incapable; Go generally follows a more minimal approach and has many small, focused libraries instead of one large framework.

Dependency Injection

While working on large codebases, this is the most helpful thing I’ve seen. The Spring Framework automatically manages all dependencies for you. In Spring Boot, dependencies are wired automatically through annotations like @Service and @Autowired. See this code example.

1@Service
2public class OrderService {
3    private final PaymentService paymentService;
4
5    public OrderService(PaymentService paymentService) {
6        this.paymentService = paymentService;
7    }
8}

or even simpler

1@Service
2public class OrderService {
3
4    @Autowired
5    private PaymentService paymentService;
6}

Using @Autowired is not recommended, but at least it works.

In Go, dependency wiring is usually done manually through constructors. This makes the dependency graph explicit but also means the application bootstrap code grows as the system gets larger.

 1type PaymentService struct {
 2}
 3
 4func NewPaymentService() *PaymentService {
 5	return &PaymentService{}
 6}
 7
 8type OrderService struct {
 9	paymentService *PaymentService
10}
11
12func NewOrderService(paymentService *PaymentService) *OrderService {
13	return &OrderService{
14		paymentService: paymentService,
15	}
16}
17
18func main() {
19	paymentService := NewPaymentService()
20	orderService := NewOrderService(paymentService)
21
22	_ = orderService
23}

This doesn’t bother too much right now but will bother once we have hundreds of dependencies.

Built-in Validation

In Spring Boot you get built-in validations for API requests. It helps you prevent ugly if/else statements.

1public class CreateUserRequest {
2
3    @NotNull
4    @Email
5    private String email;
6}

These annotations can validate that the field is not null and that its value matches the validator’s email-format rules. They are validation rules, not proof that the mailbox actually exists.

While in Go you would need to do something like this:

 1type CreateUserRequest struct {
 2  Email string `json:"email"`
 3}
 4
 5// Validations
 6if req.Email == "" {
 7  // Handle Error
 8}
 9
10var emailRegex = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
11
12if emailRegex.MatchString(req.Email) == false {
13  // Handle Invalid Email
14}

This regex is only a lightweight format check. Email address syntax is more complicated than this pattern, and a regex match does not prove that the mailbox actually exists.

Spring’s Highly Mature Ecosystem

In order to build a backend system, there are some essential things such as auth & security, database access, and health checks. If we build something modern, the system will need microservices, an API gateway, load balancing, service discovery, etc.

As Spring Boot comes with a “batteries included” mindset, it has all the tools required for fulfilling the above requirements.

Spring Security

Do you need JWT-based authentication, form login, basic authentication, or OAuth 2.0 support? Spring Security provides components and integrations for these use cases. You still need to configure them and define application-specific security policy.

Spring Data

Spring Data is a part of the Spring Framework designed to significantly reduce the boilerplate code required to implement data access layers for various storage technologies. A fantastic thing about this is that it can generate queries automatically based on the method name you gave to it.

See this example:

 1@Entity
 2public class User {
 3
 4    @Id
 5    private Long id;
 6
 7    private String email;
 8    private String name;
 9}
10
11@Repository
12public interface UserRepository extends JpaRepository<User, Long> {
13
14    Optional<User> findByEmail(String email);
15
16}

That’s it. You can use the findByEmail method in your service. While in Go you would need to write a complete implementation of this method.

But one thing I would like to mention is that the Spring Data approach is not always best. Go will give you complete control and transparency over your SQL queries, hence making it easy to debug. Also, it’s hard to write complex JOINs in Spring Boot. Writing complex queries can sometimes feel less straightforward when using ORM abstractions.

Spring Boot Actuator

It provides health monitoring, metrics collection, and application insights with relatively little configuration. Go might also have tools for this, but I feel Spring Boot Actuator is more integrated as being a part of a large framework.

Spring Cloud

Now we come to microservices and distributed systems. Spring Cloud provides a collection of tools and integrations for building and operating distributed applications and microservices.

Where Go Actually Feels Better

After working with Spring Boot for around one and a half years, moving to Go made me appreciate a very different philosophy: simplicity over abstraction.

Spring Boot tries to reduce boilerplate through frameworks and automation. Go takes a different approach — it removes abstraction layers and encourages explicit code.

In practice, this leads to some very interesting advantages.

Simpler Operational Model

Spring Boot applications run on the JVM. That means you deal with things like JVM startup time and memory tuning & GC tuning in some cases.

Go services are just compiled binaries. You build once and run.

go build ./service

Deployment becomes extremely simple. A Go service can often be shipped as a single static binary, which makes container images smaller and startup times significantly faster.

Instant Startup

Spring Boot startup time depends on the application, dependencies, JVM, hardware, and configuration; larger applications can take noticeably longer to start than small Go binaries. Go services typically start almost instantly. This becomes useful when running serverless workloads, auto scaling systems, short-lived background workers, etc.

In my local development experience, starting a Spring Boot debugger often took longer than starting a small Go service. That difference makes Go more convenient for some of my workflows.

Concurrency Is Awesome

In Go, concurrency feels very effective because goroutines are a language-level abstraction rather than a framework feature. Goroutines are lightweight units of execution that the Go runtime schedules onto operating-system threads.

You need to create a goroutine? Put go before a function call.

1go processOrder(order)

Go also has something called ‘channels’ that are used to communicate between goroutines. Trust me, when writing highly concurrent systems, channels make things straightforward and easy.

I’ve heard that Java also introduced something called ‘virtual threads’ that makes concurrency easier and lightweight in Java as well. But I’m yet to try them.

In my experience, Go can have a smaller resource footprint for some services, particularly because the runtime model is different from the JVM. The actual memory behavior depends heavily on the workload, so I would compare the two using measurements from a representative application rather than a blanket claim.

Being able to produce a small, statically linked binary is also nice for distribution.

Builds tend to be quick. Go builds packages in parallel where possible and has built-in build caching. Java projects can also parallelize work through their build tools, so the exact build-time difference depends on the project and tooling.

So Which Is Best?

Actually, this is the wrong question. It’s not about which language is better. It’s about what kind of systems you are trying to build.

If I were building a large enterprise platform with complex domain logic, Spring Boot would still be a fantastic choice.

But if I were building infrastructure tools, high-concurrency systems, or lightweight services, Go feels incredibly natural.

Examples of Systems That Use Java

  • Netflix
  • Amazon
  • LinkedIn
  • Uber
  • Spotify

Examples of Systems That Use Go

  • Docker
  • Kubernetes
  • Prometheus
  • etcd
  • CockroachDB
  • Cloudflare
  • Dropbox

Before You Go

If you made it this far, Thank You.

I usually write about backend engineering, distributed systems, and things I learn while working on real problems. Not theory — mostly practical stuff that I wish someone had explained to me earlier.

I run a free newsletter where I share these kinds of write-ups. No spam. Just occasional backend engineering notes.