Skip to main content

Your First Application

Build a complete Sproogy application from scratch in under 10 minutes.

What We'll Build

A simple user management application with:

  • Dependency injection
  • Configuration management
  • A service layer with business logic
  • Console output (no socket server yet)

This will demonstrate Sproogy's core IoC/DI features before we add socket communication.

Step 1: Project Setup

Create a new Gradle project:

mkdir my-first-sproogy-app
cd my-first-sproogy-app
gradle init --type java-application

Or create manually with this structure:

my-first-sproogy-app/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ └── resources/
│ └── application.yml
├── .env
└── build.gradle

Step 2: Add Sproogy Dependencies

build.gradle:

plugins {
id 'java'
id 'application'
}

group = 'com.example'
version = '1.0.0'

sourceCompatibility = '11'

repositories {
mavenCentral()
mavenLocal()
}

dependencies {
implementation 'com.sproogy:core:1.0.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'io.github.cdimascio:dotenv-java:3.0.0'
implementation 'org.slf4j:slf4j-simple:2.0.7'
}

application {
mainClass = 'com.example.Main'
}

Step 3: Create Configuration Files

.env:

APP_NAME=My First Sproogy App
APP_VERSION=1.0.0
MAX_USERS=100

src/main/resources/application.yml:

application:
features:
enableNotifications: true
enableAuditLog: false
limits:
maxLoginAttempts: 3

Step 4: Create Domain Model

src/main/java/com/example/model/User.java:

package com.example.model;

public class User {
private Long id;
private String username;
private String email;
private boolean active;

public User() {}

public User(Long id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
this.active = true;
}

// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }

@Override
public String toString() {
return "User{id=" + id + ", username='" + username + "', email='" + email + "', active=" + active + "}";
}
}

Step 5: Create Repository Layer

src/main/java/com/example/repository/UserRepository.java:

package com.example.repository;

import com.example.model.User;
import com.sproogy.annotations.Component;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Component
public class UserRepository {

private final Map<Long, User> users = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);

public User save(User user) {
if (user.getId() == null) {
user.setId(idGenerator.getAndIncrement());
}
users.put(user.getId(), user);
return user;
}

public Optional<User> findById(Long id) {
return Optional.ofNullable(users.get(id));
}

public List<User> findAll() {
return new ArrayList<>(users.values());
}

public Optional<User> findByUsername(String username) {
return users.values().stream()
.filter(u -> u.getUsername().equals(username))
.findFirst();
}

public void deleteById(Long id) {
users.remove(id);
}

public long count() {
return users.size();
}
}

Step 6: Create Service Layer

src/main/java/com/example/service/UserService.java:

package com.example.service;

import com.example.model.User;
import com.example.repository.UserRepository;
import com.sproogy.annotations.Autowired;
import com.sproogy.annotations.Env;
import com.sproogy.annotations.Service;
import com.sproogy.annotations.Value;

import java.util.List;

@Service
public class UserService {

private final UserRepository userRepository;

@Env("MAX_USERS")
private int maxUsers;

@Value("application.features.enableNotifications")
private boolean notificationsEnabled;

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

public User createUser(String username, String email) {
// Business logic: Check user limit
if (userRepository.count() >= maxUsers) {
throw new IllegalStateException("Maximum user limit reached: " + maxUsers);
}

// Business logic: Validate unique username
if (userRepository.findByUsername(username).isPresent()) {
throw new IllegalArgumentException("Username already exists: " + username);
}

User user = new User(null, username, email);
user = userRepository.save(user);

if (notificationsEnabled) {
System.out.println("[NOTIFICATION] Welcome email sent to: " + email);
}

return user;
}

public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("User not found: " + id));
}

public List<User> findAll() {
return userRepository.findAll();
}

public void deactivateUser(Long id) {
User user = findById(id);
user.setActive(false);
userRepository.save(user);

if (notificationsEnabled) {
System.out.println("[NOTIFICATION] Account deactivated for: " + user.getEmail());
}
}

public long getUserCount() {
return userRepository.count();
}
}

Step 7: Create Application Runner

src/main/java/com/example/AppRunner.java:

package com.example;

import com.example.service.UserService;
import com.sproogy.annotations.Autowired;
import com.sproogy.annotations.Component;
import com.sproogy.annotations.Env;
import com.sproogy.annotations.Value;

@Component
public class AppRunner {

@Autowired
private UserService userService;

@Env("APP_NAME")
private String appName;

@Env("APP_VERSION")
private String appVersion;

@Value("application.limits.maxLoginAttempts")
private int maxLoginAttempts;

public void run() {
System.out.println("=".repeat(50));
System.out.println(appName + " v" + appVersion);
System.out.println("Max Login Attempts: " + maxLoginAttempts);
System.out.println("=".repeat(50));
System.out.println();

// Create users
System.out.println("Creating users...");
var alice = userService.createUser("alice", "alice@example.com");
var bob = userService.createUser("bob", "bob@example.com");
var charlie = userService.createUser("charlie", "charlie@example.com");

System.out.println("✓ Created: " + alice);
System.out.println("✓ Created: " + bob);
System.out.println("✓ Created: " + charlie);
System.out.println();

// List all users
System.out.println("All users:");
userService.findAll().forEach(user -> System.out.println(" - " + user));
System.out.println("Total users: " + userService.getUserCount());
System.out.println();

// Deactivate a user
System.out.println("Deactivating Bob...");
userService.deactivateUser(bob.getId());
System.out.println("✓ Bob deactivated: " + userService.findById(bob.getId()));
System.out.println();

// Test validation
System.out.println("Testing validation...");
try {
userService.createUser("alice", "alice2@example.com");
} catch (IllegalArgumentException e) {
System.out.println("✓ Validation worked: " + e.getMessage());
}
}
}

Step 8: Create Main Class

src/main/java/com/example/Main.java:

package com.example;

import com.sproogy.SproogyApp;
import com.sproogy.annotations.SproogyApplication;
import com.sproogy.app.ApplicationContext;

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

// Get the runner bean and execute
AppRunner runner = context.getBean(AppRunner.class);
runner.run();
}
}

Step 9: Run the Application

./gradlew run

Expected Output:

==================================================
My First Sproogy App v1.0.0
Max Login Attempts: 3
==================================================

Creating users...
[NOTIFICATION] Welcome email sent to: alice@example.com
✓ Created: User{id=1, username='alice', email='alice@example.com', active=true}
[NOTIFICATION] Welcome email sent to: bob@example.com
✓ Created: User{id=2, username='bob', email='bob@example.com', active=true}
[NOTIFICATION] Welcome email sent to: charlie@example.com
✓ Created: User{id=3, username='charlie', email='charlie@example.com', active=true}

All users:
- User{id=1, username='alice', email='alice@example.com', active=true}
- User{id=2, username='bob', email='bob@example.com', active=true}
- User{id=3, username='charlie', email='charlie@example.com', active=true}
Total users: 3

Deactivating Bob...
[NOTIFICATION] Account deactivated for: bob@example.com
✓ Bob deactivated: User{id=2, username='bob', email='bob@example.com', active=false}

Testing validation...
✓ Validation worked: Username already exists: alice

What Just Happened?

Let's break down what Sproogy did:

1. Component Scanning

Sproogy scanned com.example and found:

  • UserRepository (@Component)
  • UserService (@Service)
  • AppRunner (@Component)

2. Dependency Graph

Sproogy built this dependency graph:

AppRunner → UserService → UserRepository

3. Configuration Injection

Sproogy injected:

  • From .env: APP_NAME, APP_VERSION, MAX_USERS
  • From application.yml: enableNotifications, maxLoginAttempts

4. Bean Creation Order

Sproogy created beans in dependency order:

  1. UserRepository (no dependencies)
  2. UserService (depends on UserRepository)
  3. AppRunner (depends on UserService)

5. Field Injection

After creating each bean, Sproogy injected:

  • Constructor parameters (e.g., UserRepository into UserService)
  • @Autowired fields
  • @Env fields (from environment variables)
  • @Value fields (from application.yml)

Experiment: Configuration Changes

Disable Notifications

Edit application.yml:

application:
features:
enableNotifications: false # Changed from true

Run again:

./gradlew run

Notice: No [NOTIFICATION] messages appear!

Change User Limit

Edit .env:

MAX_USERS=2  # Changed from 100

Run again. The third user creation will fail:

Exception: Maximum user limit reached: 2

Key Takeaways

🎯 Dependency Injection: Sproogy automatically wired UserRepository into UserService

🎯 Configuration: Values from .env and application.yml were injected into fields

🎯 Lifecycle: Sproogy managed the creation order based on dependencies

🎯 Testability: Each component can be tested in isolation (just pass mocks to constructors)

Testing Your Components

Here's how easy it is to unit test with dependency injection:

src/test/java/com/example/service/UserServiceTest.java:

package com.example.service;

import com.example.model.User;
import com.example.repository.UserRepository;
import org.junit.jupiter.api.Test;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class UserServiceTest {

@Test
void testCreateUser() {
// Create mock repository
UserRepository mockRepo = mock(UserRepository.class);
when(mockRepo.count()).thenReturn(0L);
when(mockRepo.findByUsername("alice")).thenReturn(Optional.empty());
when(mockRepo.save(any(User.class))).thenAnswer(i -> {
User user = i.getArgument(0);
user.setId(1L);
return user;
});

// Inject mock into service (NO Sproogy needed for testing!)
UserService service = new UserService(mockRepo);

// Test
User user = service.createUser("alice", "alice@example.com");

assertEquals(1L, user.getId());
assertEquals("alice", user.getUsername());
assertTrue(user.isActive());

verify(mockRepo).save(any(User.class));
}
}

Notice: No Sproogy framework in the test! That's the power of dependency injection.

Next Steps

Congratulations! You've built your first Sproogy application with dependency injection and configuration management.

Now let's add socket communication:

👉 Hello World Socket Server

Or dive deeper into dependency injection:

👉 Dependency Injection Deep Dive