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