Graceful application shutdown is a critical aspect of ensuring service reliability, particularly in high-load systems.
In this article, we will explore how graceful shutdown is implemented in two popular technologies: Spring Framework and Golang, using examples that interact with a PostgreSQL database.
Why Did I Choose These Two Stacks for Comparison?
It’s quite simple: when transitioning from Java and other JVM-based technologies to languages without a robust framework, developers are faced with the reality of managing many tasks manually. In Spring, many mechanisms for resource management come out of the box, while in Go, these processes need to be implemented by hand.
For example, many developers don’t realize what Spring does under the hood. On the other hand, in Gohttps://dzone.com/articles/golang-tutorial-learn-golang-by-examples, we are responsible for ensuring proper resource cleanup to prevent issues like connection leaks in the database.
Let’s take a detailed look at how both technologies handle system signals, resource closure, connections, and background tasks, followed by a thorough comparison of their approaches.
The Concept of Graceful Shutdown
Before diving into code, let’s understand what a graceful shutdown is.
Graceful shutdown is a mechanism that allows an application to terminate cleanly after receiving a shutdown signal (e.g., SIGTERM or SIGINT).
The key objectives of a graceful shutdown are:
- Complete ongoing operations without accepting new ones.
- Release resources (e.g., database connections, files, communication channels).
- Shut down servers and background tasks.
- Prevent resource leaks and errors during restarts.
Graceful Shutdown Mechanism in Spring Framework
Spring Framework implements graceful shutdown through the application’s lifecycle (ApplicationContext
) and automatic resource management.
When a Spring Boot application receives a shutdown signal (SIGTERM or SIGINT), it triggers a shutdown hook registered in the JVM:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
applicationContext.close();
}));
Steps Involved in Graceful Shutdown in Spring
1. Triggering the ContextClosedEvent
All event listeners can perform resource cleanup at this point.
@Component
public class ShutdownListener implements ApplicationListener {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("Context is closing... Cleaning up resources.");
}
}
2. Calling @PreDestroy Methods
All beans annotated with @PreDestroy
execute their shutdown logic.
@Component
public class ExampleService {
@PreDestroy
public void onDestroy() {
System.out.println("Bean is being destroyed...");
}
}
3. Closing Connection Pools
If the application uses a database with Spring Data and a connection pool (e.g., HikariCP), Spring automatically invokes the shutdown()
method on the pool.
4. Terminating Background Tasks
Tasks created with @Scheduled
or @Async
are automatically stopped.
Basically, that’s it.
Let’s see an example.
Example of Graceful Shutdown in Spring
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import javax.annotation.PreDestroy;
import java.util.logging.Logger;
@SpringBootApplication
public class Application implements CommandLineRunner {
private static final Logger logger = Logger.getLogger(Application.class.getName());
@PreDestroy
public void onShutdown() {
logger.info("Application is shutting down gracefully...");
}
@Autowired
private UserRepository userRepository;
@Override
public void run(String... args) {
logger.info("Application started.");
userRepository.findByName("Alice").ifPresent(user -> logger.info("User found: " + user.getName()));
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
In this case, developers have minimal responsibilities regarding resource management, as Spring handles most of the shutdown logic. However, for custom cleanup, methods like @PreDestroy
or implementing DisposableBean
are essential.
Graceful Shutdown Mechanism in Golang
In Go, graceful shutdown requires explicit implementation. The key steps are:
- Handling system signals.
- The
os/signal
package is used to capture termination signals.
- The
- Context and cancel functions.
context.Context
is used to control task termination.
- Manual resource cleanup.
- All open resources and connections must be closed manually using methods like
Close()
orcancel()
.
- All open resources and connections must be closed manually using methods like
Example of Graceful Shutdown in Go
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v4"
)
// Background task with context
func backgroundTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Background task stopped")
return
default:
fmt.Println("Background task running...")
time.Sleep(1 * time.Second)
}
}
}
func main() {
fmt.Println("Starting application...")
// Context for task management
ctx, cancel := context.WithCancel(context.Background())
// Database connection
conn, err := pgx.Connect(ctx, "postgres://user:password@localhost:5432/mydb")
if err != nil {
fmt.Printf("Unable to connect to database: %v\n", err)
return
}
defer conn.Close(ctx)
// Start background task
go backgroundTask(ctx)
// Wait for termination signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
fmt.Println("Shutting down gracefully...")
cancel() // Terminate tasks
fmt.Println("Application stopped.")
}
That’s not all — it’s a good idea to create something like a Closer
function that gathers all Close
methods in the main
function and then executes them.
In other words, all open connections and resources need to be closed manually by calling Close()
methods or cancel()
functions. To make this process more convenient and centralized, you can use the Closer
function.
Let’s take a look at a universal Closer
function for shutdown in Golang.
The Closer
function allows you to pass a list of termination functions (cancel()
, Close()
) and automatically invoke them in the correct order. This helps prevent errors and simplifies the code.
package main
import (
"fmt"
"sync"
)
func Closer(closeFuncs ...func()) {
var wg sync.WaitGroup
for _, closeFunc := range closeFuncs {
wg.Add(1)
go func(f func()) {
defer wg.Done()
f()
}(closeFunc)
}
wg.Wait()
}
With this function, you can clean up multiple resources in parallel:
func main() {
ctx, cancel := context.WithCancel(context.Background())
conn, _ := pgx.Connect(ctx, "postgres://user:password@localhost:5432/mydb")
// Start background task
go backgroundTask(ctx)
// Wait for termination signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
Closer(
func() { conn.Close(ctx) },
cancel,
)
fmt.Println("Application stopped.")
}
Here:
1. Context and Background Tasks
The ctx
context is used to manage tasks. When cancel()
is called, background tasks automatically stop through the ctx.Done()
channel.
2. Using the Closer Function
The Closer
function takes termination functions (cancel()
, Close()
, etc.) and executes them in parallel. In this example, the database connection is closed using conn.Close()
.
- The
cancel()
function is called to terminate all related tasks.
3. Completion Wait
- The
sync.WaitGroup
ensures that all termination functions complete before the application shuts down.
Advantages of the Closer Function
- Convenience. All resource cleanup functions can be grouped and passed in one place.
- Parallel execution. Resource cleanup operations are performed in parallel, reducing overall shutdown time.
- Safety. Using
sync.WaitGroup
prevents premature application termination by ensuring that all resources are fully released.
Important: The next section presents a simple comparison of the two implementations. If you’re a professional, you can skip directly to the conclusion. This visualization is intended for colleagues who are either beginners or transitioning from other programming languages.
Comparison
feature | spring framework | golang |
---|---|---|
Signal handling | Automatic via SpringApplication | Manual via os/signal |
Resource managemen | @PreDestroy auto management | Manual with Close() and cancel() |
Flexibility | High-level abstraction | Full control over processes and resources |
Performance overhead | Potentially higher due to abstraction layers | Minimal overhead |
Thread management | Automatic (@Async, @Scheduled) or manual green threads | Manual through goroutines |
In instance, with database simple connections:
Feature | Spring Framework (spring data) | Golang (pgx) |
---|---|---|
Database connection | Automatic via application.yml configuration | Manual with pgx.Connect() |
Transaction management | Managed by @Transactional via AOP | Transactions handled through context |
Connection pool handling | Automatic with pools like HikariCP | Manual with explicit Close() calls (but there is pgxpool) |
Error handling | Exceptions with try-catch or Error Handler by Spring, or ControllerAdvice | Errors returned from functions |
Conclusion
What should you choose? There is no single correct answer — it depends on you!
- Spring Framework offers high-level abstractions, making development faster and easier to maintain. However, this can reduce flexibility and introduce performance overhead.
- Golang, on the other hand, requires more manual work but gives you full control over processes and resources. This approach is especially beneficial for microservices and high-load systems.
In any case, exploring different programming languages and frameworks is always valuable. It helps you understand how things work elsewhere and refine your own practices.
Take care!