Skip to main content

Application Lifecycle

Understanding the Sproogy application lifecycle is crucial for knowing when to initialize resources, when dependencies are available, and how graceful shutdown works.

Lifecycle Overview

Phase 1: Validation & Configuration

1.1 Annotation Validation

Sproogy verifies your main class has @SproogyApplication:

@SproogyApplication(basePackage = "com.example")
public class Main {
public static void main(String[] args) {
SproogyApp.run(Main.class, args);
}
}

What's checked:

  • @SproogyApplication annotation is present
  • basePackage is specified

If invalid:

Exception: Main class must be annotated with @SproogyApplication

1.2 Environment Loading

Sproogy loads environment variables from .env:

// EnvLoader.load() is called automatically
EnvLoader.load();

// Now environment variables are available
String port = EnvLoader.get("SOCKET_PORT"); // "8443"

Search order:

  1. .env file in working directory
  2. System environment variables (override .env)

Example .env:

SOCKET_PORT=8443
DB_URL=jdbc:mysql://localhost:3306/mydb
DB_USER=admin
DB_PASSWORD=secret

Phase 2: Component Discovery

2.1 Component Scanning

Sproogy scans for components in:

  • Your package (from basePackage)
  • Framework packages (com.sproogy.*)

Scanned annotations:

  • @Component
  • @Service
  • @Controller
  • @Repository
  • @Configuration

Example:

@SproogyApplication(basePackage = "com.example")
// Scans: com.example.*, com.sproogy.*

2.2 Pre-DI Module Loading (SproogyLoader)

Before dependency injection, modules can prepare resources using the SproogyLoader interface.

Use case: JPA module needs to scan entities and configure Hibernate BEFORE DI creates repositories.

How it works:

  1. Sproogy uses Java ServiceLoader to discover SproogyLoader implementations
  2. Calls loader.load(components, env) for each loader
  3. Loaders can modify configuration, scan for additional classes, etc.

Example: JPA Module

The JPA module implements SproogyLoader:

// Located in: code/jpa/src/main/resources/META-INF/services/com.sproogy.SproogyLoader
com.sproogy.jpa.loader.JpaLoader
public class JpaLoader implements SproogyLoader {

@Override
public void load(Set<Class<?>> components, Map<String, String> env) {
// 1. Scan for @Entity classes
Set<Class<?>> entities = findEntities(components);

// 2. Configure Hibernate with entities
Configuration hibernateConfig = new Configuration();
for (Class<?> entity : entities) {
hibernateConfig.addAnnotatedClass(entity);
}

// 3. Store configuration for later use
// (When EntityManagerFactory bean is created)
}
}

Creating your own loader:

// 1. Implement interface
public class MyCustomLoader implements SproogyLoader {
@Override
public void load(Set<Class<?>> components, Map<String, String> env) {
System.out.println("Custom loader running before DI!");
// Prepare resources, modify configuration, etc.
}
}

// 2. Register via ServiceLoader
// File: src/main/resources/META-INF/services/com.sproogy.SproogyLoader
com.example.MyCustomLoader

Phase 3: Dependency Injection

3.1 Dependency Graph Construction

Sproogy analyzes constructor parameters and @Autowired fields to build a dependency graph.

Example:

@Controller
public class UserController {
@Autowired
private UserService userService;
}

@Service
public class UserService {
private final UserRepository userRepository;

@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {}

Dependency graph:

UserController → UserService → UserRepository

3.2 Circular Dependency Detection

Sproogy detects cycles at startup (fail-fast):

@Service
public class A {
@Autowired private B b;
}

@Service
public class B {
@Autowired private A a; // ❌ Cycle detected at startup!
}

Error:

Exception: Circular dependency detected:
A → B → A

3.3 Topological Sort

Dependencies are instantiated leaf-first:

Order:
1. UserRepository (no dependencies)
2. UserService (depends on UserRepository)
3. UserController (depends on UserService)

3.4 Bean Instantiation

For each bean (in dependency order):

  1. Constructor invocation: Sproogy calls the @Autowired constructor (or no-arg constructor)
  2. Field injection: Inject @Autowired fields via reflection
  3. Value injection: Inject @Value and @Env fields
  4. Registration: Store in ApplicationContext

Phase 4: Post-DI Initialization

4.1 Bootstrap Hooks (SproogyBootStrap)

After all beans are created, modules can initialize components using the SproogyBootStrap interface.

Use case: Server module needs to initialize controller routes AFTER DI creates controllers.

How it works:

  1. Sproogy uses ServiceLoader to discover SproogyBootStrap implementations
  2. Calls bootstrap.init(context) for each bootstrap
  3. Bootstraps can access the fully-initialized ApplicationContext

Example: Server Module

// Located in: code/socket/server/src/main/resources/META-INF/services/com.sproogy.SproogyBootStrap
com.sproogy.socket.server.bootstrap.ServerBootStrap
public class ServerBootStrap implements SproogyBootStrap {

@Override
public void init(ApplicationContext context) {
// 1. Find all @Controller beans
List<Object> controllers = findControllers(context);

// 2. Scan for @AppMapping methods
for (Object controller : controllers) {
for (Method method : controller.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(AppMapping.class)) {
String endpoint = method.getAnnotation(AppMapping.class).value();
// Register route: endpoint → method
RouteRegistry.register(endpoint, controller, method);
}
}
}

// 3. Initialize filter chain
// ...
}
}

Creating your own bootstrap:

// 1. Implement interface
public class MyCustomBootstrap implements SproogyBootStrap {
@Override
public void init(ApplicationContext context) {
System.out.println("Custom bootstrap running after DI!");
// Access beans, initialize resources, etc.
MyService myService = context.getBean(MyService.class);
myService.initialize();
}
}

// 2. Register via ServiceLoader
// File: src/main/resources/META-INF/services/com.sproogy.SproogyBootStrap
com.example.MyCustomBootstrap

4.2 Server Startup

If a ServerFactory bean exists, Sproogy automatically starts the server:

@Component
public class MyServerFactory implements ServerFactory {

@Override
public Server createServer() {
// Return configured SSL server
return new DefaultServer(...);
}
}

What happens:

  1. ServerFactory.createServer() is called
  2. server.startServer() is invoked (non-blocking)
  3. Shutdown hook is registered to call server.stopServer() on JVM exit

Phase 5: Graceful Shutdown

When the JVM exits (Ctrl+C, kill signal, etc.), Sproogy gracefully shuts down the server.

Shutdown Sequence

  1. Stop accepting new connections: serverSocket.close()
  2. Wait for active requests: Threads handling current requests are allowed to finish
  3. Close client sockets: All active connections are closed
  4. Log shutdown: "Server stopped successfully"

Shutdown hook registration:

// Automatically registered by ServerFactory
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.stopServer();
}));

Custom Shutdown Logic

Add shutdown hooks for your own resources:

@Component
public class DatabaseService {

private HikariDataSource dataSource;

public DatabaseService() {
this.dataSource = new HikariDataSource(config);

// Register shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Closing database connections...");
dataSource.close();
}));
}
}

Bean Lifecycle Timing

EventWhen It HappensWhat's Available
Constructor calledDuring Phase 3 (DI)Constructor parameters (injected dependencies)
@Autowired fields injectedAfter constructorAll injected dependencies
SproogyBootStrap.init()After all beans createdFull ApplicationContext
Server startsAfter bootstrapsEverything
Application runsAfter server startsEverything
Shutdown hooksOn JVM exitEverything (being destroyed)

Lazy Initialization

By default, all beans are created at startup. Use lazy = true to defer creation:

@Component(lazy = true)
public class HeavyResource {

public HeavyResource() {
System.out.println("Expensive initialization...");
// Load large file, connect to external service, etc.
}
}

@Service
public class MyService {

@Autowired
private HeavyResource resource; // Created only when MyService is created
}

Timing:

  • Without lazy: Created during Phase 3 (DI)
  • With lazy: Created when first injected or retrieved

Execution Order Example

Let's trace a complete application startup:

// Main.java
@SproogyApplication(basePackage = "com.example")
public class Main {
public static void main(String[] args) {
System.out.println("[1] main() called");
SproogyApp.run(Main.class, args);
System.out.println("[9] SproogyApp.run() returned");
}
}

// MyLoader.java
public class MyLoader implements SproogyLoader {
@Override
public void load(Set<Class<?>> components, Map<String, String> env) {
System.out.println("[3] SproogyLoader.load() - before DI");
}
}

// MyService.java
@Service
public class MyService {
public MyService() {
System.out.println("[5] MyService constructor");
}

@Autowired
private MyRepository repository;

// Field injected here (no way to log it)
}

// MyRepository.java
@Repository
public interface MyRepository extends JpaRepository<User, Long> {}

// MyBootstrap.java
public class MyBootstrap implements SproogyBootStrap {
@Override
public void init(ApplicationContext context) {
System.out.println("[7] SproogyBootStrap.init() - after DI");
}
}

// MyServerFactory.java
@Component
public class MyServerFactory implements ServerFactory {
@Override
public Server createServer() {
System.out.println("[8] ServerFactory.createServer()");
return new DefaultServer(...);
}
}

Console output:

[1] main() called
[2] Loading .env file
[3] SproogyLoader.load() - before DI
[4] Building dependency graph
[5] MyService constructor
[6] Injecting @Autowired fields
[7] SproogyBootStrap.init() - after DI
[8] ServerFactory.createServer()
[9] SproogyApp.run() returned
Server running on port 8443...

Common Patterns

Pattern 1: Initialization Order Control

If ServiceA must initialize before ServiceB:

@Service
public class ServiceA {
public ServiceA() {
System.out.println("ServiceA init");
}
}

@Service
public class ServiceB {
private final ServiceA serviceA;

@Autowired
public ServiceB(ServiceA serviceA) {
// ServiceA constructor has already run
System.out.println("ServiceB init");
this.serviceA = serviceA;
}
}

Sproogy guarantees ServiceA is created before ServiceB (dependency graph).

Pattern 2: Resource Initialization in Bootstrap

For resources that need the full ApplicationContext:

public class CacheBootstrap implements SproogyBootStrap {

@Override
public void init(ApplicationContext context) {
CacheManager cache = context.getBean(CacheManager.class);
cache.loadInitialData(); // Needs all services to be ready
}
}

Pattern 3: Cleanup on Shutdown

Always clean up resources:

@Component
public class ConnectionPool {

private final List<Connection> connections = new ArrayList<>();

public ConnectionPool() {
// Initialize connections
for (int i = 0; i < 10; i++) {
connections.add(createConnection());
}

// Register cleanup
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
connections.forEach(Connection::close);
}));
}
}

Next Steps

Now that you understand the lifecycle, explore all available annotations:

👉 Annotations Reference

Or start building with the Getting Started guide:

👉 Installation