Skip to main content

Best Practices

Production-ready patterns and recommendations for building robust Sproogy applications.

Application Structure

Organize by Feature

src/main/java/com/example/
├── user/
│ ├── User.java (Entity)
│ ├── UserRepository.java
│ ├── UserService.java
│ └── UserController.java
├── order/
│ ├── Order.java
│ ├── OrderRepository.java
│ ├── OrderService.java
│ └── OrderController.java
└── config/
├── SSLConfiguration.java
└── DatabaseConfiguration.java

Benefits: Related code stays together, easier to navigate.

Layer Responsibilities

Controller → Service → Repository
↓ ↓ ↓
Routing Business Data
Logic Access

Rules:

  • Controllers: Handle requests, validate input, return responses
  • Services: Business logic, transactions, orchestration
  • Repositories: Data access only

Dependency Injection

Prefer Constructor Injection

// ✅ Good: Immutable dependencies
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;

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

// ❌ Avoid: Mutable dependencies
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Can be null, not final
}

Keep Dependencies Minimal

// ❌ Bad: Too many dependencies
@Service
public class OrderService {
@Autowired private OrderRepository orderRepo;
@Autowired private UserRepository userRepo;
@Autowired private ProductRepository productRepo;
@Autowired private PaymentService paymentService;
@Autowired private EmailService emailService;
@Autowired private NotificationService notificationService;
@Autowired private AuditService auditService;
// ... this is a code smell!
}

// ✅ Good: Refactored
@Service
public class OrderService {
@Autowired private OrderRepository orderRepo;
@Autowired private OrderOrchestrator orchestrator; // Encapsulates other services
}

Error Handling

Use Specific Exceptions

// ❌ Bad
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Not found")); // Too generic
}

// ✅ Good
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}

// Custom exception
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found with id: " + id);
}
}

Handle Errors in Filters

@Component
public class ErrorHandlingFilter implements Filter {
@Override
public void doFilter(Request req, Response res, FilterChain chain) throws Exception {
try {
chain.doFilter(req, res);
} catch (EntityNotFoundException e) {
res.setStatus(404);
res.setData(Map.of("error", e.getMessage()));
} catch (ValidationException e) {
res.setStatus(400);
res.setData(Map.of("error", e.getMessage()));
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
res.setStatus(500);
res.setData(Map.of("error", "Internal server error"));
}
}
}

Security

Always Validate Input

@AppMapping("/users")
public ResponseEntity<User> createUser(CreateUserRequest request) {
// Validate before processing
if (request.getUsername() == null || request.getUsername().length() < 3) {
return ResponseEntity.status(400).build();
}

if (!isValidEmail(request.getEmail())) {
return ResponseEntity.status(400).build();
}

return ResponseEntity.ok(userService.createUser(request));
}

Use Authentication Filters

@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(Request req, Response res, FilterChain chain) {
// Whitelist public endpoints
if (isPublicEndpoint(req.getEndpoint())) {
chain.doFilter(req, res);
return;
}

// Require authentication for all other endpoints
String token = req.getHeaders().get("Authorization");
if (!tokenService.validate(token)) {
res.setStatus(401);
return;
}

chain.doFilter(req, res);
}
}

Never Log Sensitive Data

// ❌ Bad
System.out.println("User password: " + user.getPassword()); // Security breach!

// ✅ Good
System.out.println("User created: " + user.getUsername());

Database

Use Transactions for Writes

// ✅ Always use @Transactional for data modifications
@Transactional
public void transferFunds(Long fromId, Long toId, BigDecimal amount) {
// Multiple DB operations = one transaction
Account from = accountRepo.findById(fromId).orElseThrow();
Account to = accountRepo.findById(toId).orElseThrow();

from.debit(amount);
to.credit(amount);

accountRepo.save(from);
accountRepo.save(to);
// Commits together or rolls back together
}

Avoid N+1 Queries

// ❌ Bad: N+1 query problem
@Transactional
public List<UserDTO> getAllUsers() {
List<User> users = userRepo.findAll(); // 1 query
return users.stream()
.map(user -> new UserDTO(
user.getUsername(),
user.getOrders().size() // N queries (one per user)!
))
.collect(Collectors.toList());
}

// ✅ Good: Single query with JOIN FETCH
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders(); // 1 query total
}

Use Pagination for Large Results

// ❌ Bad: Load everything
List<User> users = userRepo.findAll(); // Could be millions!

// ✅ Good: Paginate
Page<User> page = userRepo.findAll(PageRequest.of(0, 20)); // First 20 users

Performance

Cache Expensive Operations

@Service
public class ConfigService {
private Map<String, Object> cache = new ConcurrentHashMap<>();

public Object getConfig(String key) {
return cache.computeIfAbsent(key, k -> loadFromDatabase(k));
}
}

Use Connection Pooling

@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource(@Env("DB_URL") String url) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(20); // Tune based on load
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
return new HikariDataSource(config);
}
}

Monitor Performance

@Component
public class MetricsFilter implements Filter {
private final Map<String, LongAdder> requestCounts = new ConcurrentHashMap<>();
private final Map<String, LongAdder> totalDurations = new ConcurrentHashMap<>();

@Override
public void doFilter(Request req, Response res, FilterChain chain) {
long start = System.nanoTime();
chain.doFilter(req, res);
long duration = System.nanoTime() - start;

String endpoint = req.getEndpoint();
requestCounts.computeIfAbsent(endpoint, k -> new LongAdder()).increment();
totalDurations.computeIfAbsent(endpoint, k -> new LongAdder()).add(duration);
}

public Map<String, Double> getAverageDurations() {
Map<String, Double> averages = new HashMap<>();
for (String endpoint : requestCounts.keySet()) {
long count = requestCounts.get(endpoint).sum();
long total = totalDurations.get(endpoint).sum();
averages.put(endpoint, (double) total / count / 1_000_000); // ms
}
return averages;
}
}

Testing

Test Each Layer Independently

// Controller test (mock service)
@Test
public void testGetUser() {
UserService mockService = mock(UserService.class);
when(mockService.findById(1L)).thenReturn(new User("alice"));

UserController controller = new UserController(mockService);
ResponseEntity<User> response = controller.getUser(1L);

assertEquals("alice", response.getData().getUsername());
}

// Service test (mock repository)
@Test
public void testCreateUser() {
UserRepository mockRepo = mock(UserRepository.class);
when(mockRepo.save(any())).thenAnswer(i -> i.getArgument(0));

UserService service = new UserService(mockRepo);
User user = service.createUser("alice", "alice@example.com");

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

Configuration

Use Environment Variables for Secrets

# .env (never commit this file!)
DB_PASSWORD=super-secret
JWT_SECRET=very-secret-key

# application.yml (safe to commit)
application:
features:
enableCache: true

Separate Configs by Environment

src/main/resources/
├── application.yml (shared config)
├── application-dev.yml (development)
├── application-prod.yml (production)
└── .env (local secrets)

Code Quality

Write Self-Documenting Code

// ❌ Bad: Magic numbers
if (user.getAge() > 18) { }

// ✅ Good: Named constants
private static final int LEGAL_AGE = 18;
if (user.getAge() > LEGAL_AGE) { }

// ❌ Bad: Unclear variable names
List<User> u = userRepo.findAll();

// ✅ Good: Clear names
List<User> activeUsers = userRepo.findByActive(true);

Keep Methods Small

// ❌ Bad: 100-line method doing everything

// ✅ Good: Small, focused methods
@Transactional
public Order createOrder(OrderRequest request) {
validateRequest(request);
Order order = buildOrder(request);
order = orderRepo.save(order);
notifyCustomer(order);
return order;
}

private void validateRequest(OrderRequest request) { }
private Order buildOrder(OrderRequest request) { }
private void notifyCustomer(Order order) { }

Next Steps

👉 Common Issues 👉 FAQ