⬅️ Back to Layers Overview

Best Practices

1. Keep Services Focused

Each service handles one business domain. Do not mix unrelated responsibilities.

// ✅ Good - Each service focused on one domain
@Service
public class SupplierServiceImpl implements SupplierService {
    // Only supplier operations
    public SupplierDTO create(CreateSupplierDTO dto) { }
    public SupplierDTO update(String id, UpdateSupplierDTO dto) { }
    public void delete(String id) { }
}

@Service
public class InventoryItemServiceImpl implements InventoryItemService {
    // Only inventory item operations
    public InventoryItemDTO create(CreateInventoryItemDTO dto) { }
    public InventoryItemDTO updateStock(String id, int newQuantity) { }
}

@Service
public class AnalyticsServiceImpl implements AnalyticsService {
    // Only analytics operations
    public DashboardSummaryDTO getDashboardSummary() { }
    public FinancialSummaryDTO getFinancialSummary() { }
}

// ❌ Bad - Mixed responsibilities
@Service
public class AllInOneService {
    // Supplier operations
    public SupplierDTO createSupplier(CreateSupplierDTO dto) { }
    
    // Inventory operations
    public InventoryItemDTO createItem(CreateInventoryItemDTO dto) { }
    
    // Analytics
    public DashboardSummaryDTO getDashboard() { }
    
    // Too many responsibilities
}

2. Use Constructor Injection

Always inject dependencies via constructor, never use the new keyword.

// ✅ Good - Constructor injection
@Service
@RequiredArgsConstructor
public class SupplierServiceImpl implements SupplierService {
    
    private final SupplierRepository repository;
    private final SupplierValidator validator;
    private final SupplierMapper mapper;
    
    // Constructor auto-generated by Lombok
    // Fully testable with mocked dependencies
}

// ❌ Bad - Service locator pattern
@Service
public class SupplierServiceImpl implements SupplierService {
    
    private SupplierRepository repository = 
        ServiceLocator.getService(SupplierRepository.class);
    // Hard to test, hidden dependencies
}

// ❌ Bad - Field injection
@Service
public class SupplierServiceImpl implements SupplierService {
    
    @Autowired
    private SupplierRepository repository;
    // Testability issues with NullPointerExceptions
}

// ❌ Bad - Manual instantiation
@Service
public class SupplierServiceImpl implements SupplierService {
    
    private SupplierRepository repository = new SupplierRepository();
    // Cannot test with mocks, tightly coupled
}

3. Mark Write Operations with @Transactional

All write operations must be explicitly transactional:

// ✅ Good - Transactional writes
@Service
@RequiredArgsConstructor
public class SupplierServiceImpl implements SupplierService {
    
    private final SupplierRepository repository;
    
    @Transactional
    public SupplierDTO create(CreateSupplierDTO dto) {
        return mapper.toDTO(repository.save(mapper.toEntity(dto)));
    }
    
    @Transactional
    public SupplierDTO update(String id, UpdateSupplierDTO dto) {
        Supplier supplier = repository.findById(id)
            .orElseThrow(() -> new NoSuchElementException("Not found"));
        supplier.setName(dto.getName());
        return mapper.toDTO(supplier);  // Auto-persisted by transaction
    }
    
    @Transactional
    public void delete(String id) {
        repository.deleteById(id);
    }
    
    // Read-only query (optional optimization)
    @Transactional(readOnly = true)
    public SupplierDTO findById(String id) {
        return repository.findById(id)
            .map(mapper::toDTO)
            .orElseThrow(() -> new NoSuchElementException("Not found"));
    }
}

// ❌ Bad - Missing @Transactional
@Service
public class SupplierServiceImpl implements SupplierService {
    
    public SupplierDTO create(CreateSupplierDTO dto) {
        // No transaction - inconsistent state if failure occurs
        return mapper.toDTO(repository.save(mapper.toEntity(dto)));
    }
}

4. Let Exceptions Propagate

Don’t catch exceptions in services unless you need to transform them:

// ✅ Good - Let exceptions propagate
@Service
@RequiredArgsConstructor
public class SupplierServiceImpl implements SupplierService {
    
    @Transactional
    public SupplierDTO create(CreateSupplierDTO dto) {
        // Validation throws exception if fails
        validator.validateUniquenessOnCreate(dto.getName());
        
        // Repository throws exception if fails
        Supplier saved = repository.save(mapper.toEntity(dto));
        
        // Exception propagates to controller → GlobalExceptionHandler
        return mapper.toDTO(saved);
    }
}

// ❌ Bad - Catching and swallowing exceptions
@Service
public class SupplierServiceImpl implements SupplierService {
    
    public SupplierDTO create(CreateSupplierDTO dto) {
        try {
            return mapper.toDTO(repository.save(mapper.toEntity(dto)));
        } catch (Exception e) {
            // Lost error information
            return null;
        }
    }
}

// ❌ Bad - Catching without re-throwing
@Service
public class SupplierServiceImpl implements SupplierService {
    
    public SupplierDTO create(CreateSupplierDTO dto) {
        try {
            validator.validateUniquenessOnCreate(dto.getName());
        } catch (IllegalStateException e) {
            // Silent failure - no error returned to client
            log.error("Validation failed", e);
        }
        
        return mapper.toDTO(repository.save(mapper.toEntity(dto)));
    }
}

5. Validate Early

Validate all inputs before any persistence operations:

// ✅ Good - Validate first
@Service
@RequiredArgsConstructor
@Transactional
public class SupplierServiceImpl implements SupplierService {
    
    public SupplierDTO create(CreateSupplierDTO dto) {
        // 1. Validate input format
        validator.validateRequiredFields(dto);
        
        // 2. Validate business rules (requires DB lookup)
        validator.validateUniquenessOnCreate(dto.getName());
        
        // 3. Only THEN persist
        Supplier supplier = mapper.toEntity(dto);
        return mapper.toDTO(repository.save(supplier));
    }
}

// ❌ Bad - Persistence before validation
@Service
@Transactional
public class SupplierServiceImpl implements SupplierService {
    
    public SupplierDTO create(CreateSupplierDTO dto) {
        // 1. Persist immediately
        Supplier supplier = repository.save(mapper.toEntity(dto));
        
        // 2. Validate too late
        validator.validateUniquenessOnCreate(dto.getName());
        // If validation fails here, data already persisted
        
        return mapper.toDTO(supplier);
    }
}

Service Checklist

Before deploying a new service, verify:


⬅️ Back to Layers Overview