But Someone Still Has to Write the Code, Right? The Truth About Frameworks (Part 4)
In Parts 1-3, we learned why Spring Data JPA uses interfaces and generates implementations. But here's the critical question: "If implementations need to exist somewhere, aren't we just hiding the work? Someone still has to write the code!" Let's be brutally honest about what frameworks actually save you.
The Questionβ
"You say Spring Data generates implementations automatically, but those implementations still exist somewhere in the codebase. So it doesn't really save us from writing boilerplate codeβeventually, we need an implementation. Explain this to me."
Great question! This gets at the heart of what frameworks actually do.
The Truth: Code IS Written, But Not By Youβ
Let's be completely honest: Yes, the implementation code absolutely exists.
But here's the crucial distinction:
What You Writeβ
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
}
4 lines of code.
What Spring Generates (Dynamically at Runtime)β
// You NEVER see this - Spring creates it in memory at runtime
public class UserRepository$Proxy implements UserRepository {
@Autowired
private EntityManager em;
@Autowired
private TransactionManager txManager;
@Override
public Optional<User> findByEmail(String email) {
// Transaction management
Transaction tx = txManager.getTransaction();
tx.begin();
try {
// SQL generation from method name
TypedQuery<User> query = em.createQuery(
"SELECT u FROM User u WHERE u.email = :email",
User.class
);
query.setParameter("email", email);
// Result handling
Optional<User> result = query.getResultStream().findFirst();
// Transaction commit
tx.commit();
return result;
} catch (Exception e) {
// Exception handling
tx.rollback();
throw new DataAccessException("Failed to find user by email", e);
}
}
@Override
public Optional<User> findByUsername(String username) {
Transaction tx = txManager.getTransaction();
tx.begin();
try {
TypedQuery<User> query = em.createQuery(
"SELECT u FROM User u WHERE u.username = :username",
User.class
);
query.setParameter("username", username);
Optional<User> result = query.getResultStream().findFirst();
tx.commit();
return result;
} catch (Exception e) {
tx.rollback();
throw new DataAccessException("Failed to find user by username", e);
}
}
@Override
public User save(User user) {
Transaction tx = txManager.getTransaction();
tx.begin();
try {
User saved;
if (user.getId() == null) {
// New entity - INSERT
em.persist(user);
saved = user;
} else {
// Existing entity - UPDATE
saved = em.merge(user);
}
tx.commit();
return saved;
} catch (Exception e) {
tx.rollback();
throw new DataAccessException("Failed to save user", e);
}
}
@Override
public Optional<User> findById(Long id) {
// Implementation...
}
@Override
public List<User> findAll() {
// Implementation...
}
@Override
public void delete(User user) {
Transaction tx = txManager.getTransaction();
tx.begin();
try {
// Handle detached entities (subtle edge case!)
User managed = em.contains(user) ? user : em.merge(user);
em.remove(managed);
tx.commit();
} catch (Exception e) {
tx.rollback();
throw new DataAccessException("Failed to delete user", e);
}
}
@Override
public long count() {
// Implementation...
}
@Override
public boolean existsById(Long id) {
// Implementation...
}
// ... 20+ more methods!
}
~500 lines of code (transaction management, SQL generation, exception handling, edge cases).
The code exists, but YOU didn't write it!
So What Are We Actually Saving?β
You're right: code doesn't disappear. But what you're saving is crucial:
1. Code You Have to Maintainβ
Manual class (you own it):
@Repository
public class UserRepositoryImpl {
// YOU maintain this
// When bugs appear, YOU fix them
// When requirements change, YOU update it
// When Spring updates, YOU adapt it
}
Spring Data interface:
public interface UserRepository extends JpaRepository<User, Long> {
// Spring team maintains the implementation
// Bugs get fixed by framework team
// Updates happen automatically
}
Who maintains the code:
- Manual: You (1 developer)
- Spring Data: Spring team (hundreds of expert developers)
2. Code You Have to Testβ
Manual class:
@Repository
public class UserRepositoryImpl {
public Optional<User> findByEmail(String email) {
// Your implementation
}
public User save(User user) {
// Your implementation
}
// 30+ more methods...
}
// YOU have to write tests:
@Test
public void testFindByEmail() { }
@Test
public void testSave_NewUser() { }
@Test
public void testSave_ExistingUser() { }
@Test
public void testDelete_DetachedEntity() { }
// ... 100+ test cases!
Spring Data interface:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// Tests already exist:
// - Spring Data has comprehensive test suite
// - Used by thousands of production apps
// - Edge cases already discovered and fixed
Who tests the code:
- Manual: You (write all test cases)
- Spring Data: Already tested by Spring team + community
3. Code That Can Have Edge Case Bugsβ
Look at this seemingly simple delete() method:
// Seems simple, right?
public void delete(User user) {
em.remove(user); // β Subtle bug!
}
Problem: If user is detached (not currently managed by EntityManager), this throws an exception!
Correct implementation:
public void delete(User user) {
// Handle detached entities
User managed = em.contains(user) ? user : em.merge(user);
em.remove(managed);
}
Did you know this edge case? Most developers don't until they hit it in production.
Spring's implementation handles:
- Detached entities
- Lazy loading issues
- Transaction boundaries
- Cascade operations
- Orphan removal
- And dozens more edge cases
Who handles edge cases:
- Manual: You discover them (in production!)
- Spring Data: Already handled (discovered by community)
4. Boilerplate That Repeats Across Every Entityβ
You have multiple entities:
UserProductOrderCategoryReviewInvoice
Manual approach:
@Repository
public class UserRepositoryImpl {
// 100 lines of code
}
@Repository
public class ProductRepositoryImpl {
// 100 lines of code (copy-paste!)
}
@Repository
public class OrderRepositoryImpl {
// 100 lines of code (copy-paste!)
}
@Repository
public class CategoryRepositoryImpl {
// 100 lines of code (copy-paste!)
}
// Total: 600+ lines of repetitive code!
Spring Data approach:
public interface UserRepository extends JpaRepository<User, Long> { }
public interface ProductRepository extends JpaRepository<Product, Long> { }
public interface OrderRepository extends JpaRepository<Order, Long> { }
public interface CategoryRepository extends JpaRepository<Category, Long> { }
// Total: 4 lines!
What you're saving:
- Manual: 600 lines of repetitive code
- Spring Data: 4 lines of interfaces
The Real Benefit: Framework Handles Plumbingβ
The House Analogyβ
Manual (writing the class yourself):
You're building a house and installing all plumbing:
- β Design the plumbing system from scratch
- β Install pipes, fixtures, valves yourself
- β Maintain the system when things break
- β When a pipe breaks at 2 AM, YOU fix it
- β Takes months to build correctly
- β Every house (entity) needs full plumbing installation
Spring Data (using the interface):
You're building a house with a professional plumber:
- β Professional plumber (Spring team) designed the system
- β You just say "I need a bathroom here"
- β Spring installs all the plumbing (runtime generation)
- β Professional team maintains it
- β Bugs are fixed for ALL houses (all apps)
- β Takes minutes to specify what you need
You're not avoiding the workβyou're delegating it to specialists.
Your Code Burden Shiftsβ
Without Spring Data (100% Your Responsibility)β
@Repository
public class UserRepositoryImpl {
@Autowired
private EntityManager em;
public Optional<User> findByEmail(String email) {
// 1. Write SQL query β YOU
TypedQuery<User> query = em.createQuery(
"SELECT u FROM User u WHERE u.email = :email",
User.class
);
// 2. Handle parameters β YOU
query.setParameter("email", email);
// 3. Handle results β YOU
List<User> results = query.getResultList();
// 4. Handle empty results β YOU
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}
@Transactional // 5. Manage transactions β YOU
public User save(User user) {
// 6. Determine INSERT vs UPDATE β YOU
if (user.getId() == null) {
em.persist(user);
return user;
} else {
return em.merge(user);
}
}
// 7. Write 30+ more methods β YOU
// 8. Handle connection pooling β YOU (via config)
// 9. Implement caching β YOU
// 10. Handle all edge cases β YOU
// 11. Write tests for everything β YOU
}
Your time distribution:
- 60% writing database plumbing
- 40% writing business logic
With Spring Data (Shared Responsibility)β
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email); // β ONLY thing YOU write
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User registerUser(String email, String password) {
// ONLY business logic - no database plumbing!
if (userRepository.existsByEmail(email)) {
throw new DuplicateEmailException("Email already exists");
}
User user = new User();
user.setEmail(email);
user.setPassword(hashPassword(password));
return userRepository.save(user);
}
private String hashPassword(String password) {
// Business logic
}
}
Your responsibility:
- β Business logic (validation, password hashing, etc.)
- β Custom queries (if method naming isn't enough)
Spring's responsibility:
- β SQL generation
- β Result mapping
- β Transaction management
- β Connection pooling
- β Caching
- β Edge case handling
Your time distribution:
- 10% defining data access (interface methods)
- 90% writing business logic β This is where value is!
A Concrete Example: Bug Fixesβ
Scenario: Critical Connection Pool Bugβ
Spring discovers a critical bug in connection pooling that can cause memory leaks.
Manual Class Approachβ
What happens:
1. Spring releases new version with fix
2. You update Spring dependency in pom.xml
3. Your manual UserRepositoryImpl still has old code
4. YOU have to manually update your repository implementation
5. YOU have to test the changes
6. YOU deploy
Impact:
- β° Time to fix: 1-2 days (per repository)
- π Affected apps: Just yours
- π Risk: You might introduce new bugs while fixing
Spring Data Interface Approachβ
What happens:
1. Spring releases new version with fix
2. You update Spring dependency in pom.xml
3. Spring's generated implementation automatically uses new code
4. You deploy
Impact:
- β° Time to fix: 5 minutes (version bump)
- π Affected apps: Thousands worldwide get the fix
- π Risk: Zero (Spring team tested it)
The Multiplication Effectβ
Manual approach:
- You have 10 repositories
- Each needs the fix
- 10 Γ 2 days = 20 days of work
Spring Data approach:
- All 10 repositories use Spring Data
- Update one dependency
- 5 minutes of work
The Philosophical Differenceβ
You're absolutely right: Code has to be written somewhere.
The question is:
Should YOU Write It?β
Pros:
- β Full control over implementation
- β Can micro-optimize for your specific use case
- β No "magic" - you understand everything
Cons:
- β Time-consuming (60% of code is plumbing)
- β Error-prone (edge cases you don't know about)
- β Repetitive (copy-paste across entities)
- β Maintenance burden (YOU fix all bugs)
- β Testing burden (YOU write all tests)
Or Should Framework Write It?β
Pros:
- β Saves time (90% less code to write)
- β Battle-tested (used by thousands of apps)
- β Automatic updates (bug fixes without your effort)
- β Focus on business logic (not plumbing)
- β Industry best practices (Spring team expertise)
Cons:
- β Less control (can't change internal implementation)
- β "Magic" (harder to debug when things go wrong)
- β Framework dependency (tied to Spring)
Most teams choose Spring Data because:
Business logic matters more than controlling database plumbing.
What You're Actually Tradingβ
You're NOT Tradingβ
β "Lines of code written in the universe"
Code still gets written - just not by you.
You ARE Tradingβ
β Who writes it:
- Manual: You (1 developer)
- Framework: Spring team (100+ expert developers)
β Who maintains it:
- Manual: You (ongoing burden)
- Framework: Spring team (automatic updates)
β How many apps benefit:
- Manual: One (yours)
- Framework: Thousands (community)
β Where you spend your time:
- Manual: 60% plumbing, 40% business logic
- Framework: 10% data access, 90% business logic
The Trade-Offs (Be Honest)β
You Gainβ
Time:
- Manual: 600 lines Γ 6 entities = 3600 lines
- Spring Data: 30 lines (custom methods only)
- Saved: 3570 lines you don't write/maintain/test
Reliability:
- Battle-tested by thousands of production apps
- Edge cases already discovered and handled
- Continuous improvements from community
Focus:
- 90% time on business logic (what makes your app unique)
- 10% time on data access (generic plumbing)
You Loseβ
Control:
- Can't change how Spring generates SQL
- Can't micro-optimize internal implementation
- Tied to Spring's update schedule
Transparency:
- "Magic" proxy generation harder to debug
- Stack traces show generated code
- Need to understand Spring Data's behavior
Flexibility:
- For 99% of cases, Spring Data is perfect
- For 1% of edge cases, might need
@Queryor custom implementation
When to Choose Eachβ
Choose Spring Data When:β
β Standard CRUD operations (99% of apps)
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByActiveTrue();
}
// Perfect for standard operations
β Quick development (startups, MVPs) β Small to medium apps (typical web applications) β Team prefers convention over configuration
Choose Manual Implementation When:β
β Extreme performance optimization needed
// Hand-tuned SQL with database-specific features
@Query(value = "SELECT * FROM users USE INDEX (email_idx) WHERE email = ?1", nativeQuery = true)
User findByEmailOptimized(String email);
β Complex queries (Spring Data's method naming can't express) β Database-specific features (PostgreSQL arrays, JSON columns, etc.) β Learning purposes (understanding how ORMs work)
Choose Hybrid Approach (Best of Both Worlds)β
public interface UserRepository extends JpaRepository<User, Long>, CustomUserRepository {
// Spring Data handles simple cases
Optional<User> findByEmail(String email);
// Custom implementation for complex cases
List<User> findUsersWithComplexCriteria(SearchCriteria criteria);
}
// Custom implementation
@Repository
public class CustomUserRepositoryImpl implements CustomUserRepository {
@Autowired
private EntityManager em;
@Override
public List<User> findUsersWithComplexCriteria(SearchCriteria criteria) {
// Hand-written complex query
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
// Complex criteria...
return em.createQuery(query).getResultList();
}
}
95% Spring Data, 5% custom = Best balance!
Summaryβ
The Honest Truthβ
Yes, implementation code exists somewhere.
But the key insight is:
Code written by you (manual)
vs
Code written by framework (Spring Data)
Both create implementation.
Different:
- WHO writes it
- WHO maintains it
- WHO benefits from improvements
- WHERE your time goes
What Spring Data Actually Savesβ
NOT: "Lines of code in the universe"
YES:
- Time - 95% less code to write
- Maintenance - Framework team handles it
- Testing - Already tested by community
- Edge cases - Already discovered and fixed
- Bugs - Automatic fixes with version updates
- Focus - 90% time on business logic
The Key Questionβ
Not: "Should implementation code be written?" But: "Who should write itβyou or a team of experts?"
Most teams choose: Let experts handle generic plumbing, we'll focus on what makes our app unique.
The Real Value Propositionβ
Scenario: Building an E-Commerce Appβ
Without Spring Data (6 months):
- 2 months: Writing repository implementations
- 2 months: Testing all edge cases
- 1 month: Fixing bugs discovered in production
- 1 month: Building actual business features
With Spring Data (2 months):
- 1 week: Defining repository interfaces
- 1.5 months: Building business features
- 2 weeks: Testing business logic
- 1 week: Bug fixes
What you gained:
- β° 4 months faster to market
- π° 4 months saved cost (developer salaries)
- π More features (3x time on business logic)
- π Fewer bugs (framework handles plumbing)
Final Insight: Yes, code is written somewhere. But that "somewhere" is in a framework maintained by experts, tested by thousands of apps, and improved continuouslyβfreeing you to focus on what makes YOUR app unique. That's not hiding work; that's smart delegation. π―
Tags: #spring-boot #spring-data #jpa #frameworks #software-engineering #design-philosophy
