SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignJava & Spring Boot Best Practices
Software DesignBest Practices

Java & Spring Boot Best Practices

Master DRY vs WET vs AHA, KISS, YAGNI, and Dependency Injection from scratch — practical engineering principles every Java developer should know.

June 30, 20246 min read
javaspring-bootclean-codebest-practices

Introduction

Principles like DRY, KISS, and YAGNI sound simple. But applying them correctly in real Java and Spring Boot codebases requires nuance. This article goes beyond the acronyms to show you when each principle helps — and when blindly following it causes harm.


1. DRY vs WET vs AHA

DRY — Don't Repeat Yourself

The original idea: every piece of knowledge should have a single, unambiguous representation.

// BAD — duplicated validation in two services
public class OrderService {
    public void createOrder(Order order) {
        if (order.getAmount() <= 0) throw new IllegalArgumentException("Invalid amount");
        // ...
    }
}

public class RefundService {
    public void processRefund(Order order) {
        if (order.getAmount() <= 0) throw new IllegalArgumentException("Invalid amount");
        // ...
    }
}

// GOOD — extract shared validation
public class OrderValidator {
    public static void validateAmount(Order order) {
        if (order.getAmount() <= 0) throw new IllegalArgumentException("Invalid amount");
    }
}

WET — Write Everything Twice

Sometimes duplication is the right call. Two similar-looking blocks may serve different business purposes.

// These look similar but serve DIFFERENT business rules
public class PricingService {
    public double calculateB2BPrice(Product p) {
        return p.getBasePrice() * 0.85; // B2B gets 15% off
    }

    public double calculateB2CPrice(Product p) {
        return p.getBasePrice() * 0.90; // B2C gets 10% off
    }
}
// Merging these into one method would couple B2B and B2C pricing.
// When B2B pricing changes (e.g., volume discounts), B2C shouldn't be affected.

AHA — Avoid Hasty Abstractions

Wait until you've seen the pattern three times before abstracting. Premature DRY creates wrong abstractions that are harder to fix than duplication.

"Duplication is far cheaper than the wrong abstraction." — Sandi Metz


2. KISS — Keep It Simple, Stupid

Simple code beats clever code. Always.

Bad Code (Clever)

// One-liner that nobody can read
public boolean isEligible(User u) {
    return u != null && u.getAge() >= 18 && u.getStatus() != null 
        && "ACTIVE".equals(u.getStatus()) && !u.isBanned() 
        && (u.getKycStatus() == KycStatus.VERIFIED || u.isStaff());
}

Good Code (Simple)

public boolean isEligible(User user) {
    if (user == null) return false;
    if (user.getAge() < 18) return false;
    if (!"ACTIVE".equals(user.getStatus())) return false;
    if (user.isBanned()) return false;
    
    return user.isKycVerified() || user.isStaff();
}

Each condition is on its own line. You can set a breakpoint on any specific check. You can read the logic in 5 seconds.


3. YAGNI — You Ain't Gonna Need It

Don't build for imaginary future requirements.

Bad Code

// "We might need to support XML and YAML later"
public interface ConfigParser {
    Config parse(String input);
}

public class JsonConfigParser implements ConfigParser { ... }
public class XmlConfigParser implements ConfigParser { ... }   // Never used
public class YamlConfigParser implements ConfigParser { ... }  // Never used

public class ConfigParserFactory {
    public ConfigParser getParser(String format) { ... }  // Unnecessary complexity
}

Good Code

// We only use JSON today. Build only what you need.
public class ConfigParser {
    public Config parse(String jsonInput) {
        return objectMapper.readValue(jsonInput, Config.class);
    }
}
// If we ever need YAML, we can refactor then. It takes 30 minutes, not 3 days.

4. Dependency Injection — Explained from Scratch

Stage 1: No DI (Tightly Coupled)

public class OrderService {
    private MySQLOrderRepository repository = new MySQLOrderRepository();
    private EmailService emailService = new EmailService();

    public void placeOrder(Order order) {
        repository.save(order);
        emailService.sendConfirmation(order);
    }
}
// Problem: Cannot test without a real MySQL database and email server

Stage 2: Manual Constructor Injection

public class OrderService {
    private final OrderRepository repository;
    private final NotificationService notificationService;

    public OrderService(OrderRepository repository, NotificationService notificationService) {
        this.repository = repository;
        this.notificationService = notificationService;
    }

    public void placeOrder(Order order) {
        repository.save(order);
        notificationService.sendConfirmation(order);
    }
}

// Now we can inject mocks
OrderService service = new OrderService(
    new InMemoryOrderRepository(),  // Test double
    new NoOpNotificationService()   // Test double
);

Stage 3: Spring Boot

public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(String id);
}

@Repository
public class JpaOrderRepository implements OrderRepository {
    @Autowired private JpaRepository<OrderEntity, String> jpa;
    
    public void save(Order order) { jpa.save(toEntity(order)); }
    public Optional<Order> findById(String id) { return jpa.findById(id).map(this::toDomain); }
}

@Service
public class OrderService {
    private final OrderRepository repository;
    private final NotificationService notificationService;

    public OrderService(OrderRepository repository, NotificationService notificationService) {
        this.repository = repository;
        this.notificationService = notificationService;
    }
}

Stage 4: Testing Advantage

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock private OrderRepository repository;
    @Mock private NotificationService notificationService;
    @InjectMocks private OrderService orderService;

    @Test
    void shouldSaveOrderAndSendNotification() {
        Order order = new Order("ORD-1", "CUST-1", 500.0);

        orderService.placeOrder(order);

        verify(repository).save(order);
        verify(notificationService).sendConfirmation(order);
    }

    @Test
    void shouldNotSendNotificationIfSaveFails() {
        doThrow(new RuntimeException("DB Down")).when(repository).save(any());

        assertThrows(RuntimeException.class, () -> orderService.placeOrder(new Order()));
        verify(notificationService, never()).sendConfirmation(any());
    }
}

DI makes unit testing trivial. You test business logic in isolation, without any infrastructure.


Interview Questions

  1. When is duplication acceptable? When two pieces of code look similar but change for different reasons. Premature DRY creates wrong abstractions.

  2. What is the difference between @Autowired on a field vs constructor? Constructor injection is preferred. It makes dependencies explicit, enables immutability (final fields), and works without Spring (for testing).

  3. How do you decide between KISS and extensibility? Default to KISS. Only add extensibility when you have a concrete requirement, not a hypothetical one. It is cheaper to refactor later than to maintain unused abstractions now.


Common Mistakes

  • Field injection in Spring: Using @Autowired on fields instead of constructors. Field injection hides dependencies and makes testing harder.
  • Abstracting after one instance: Creating an interface with only one implementation "just in case." Wait for the second implementation.
  • Gold plating: Spending days making code "perfect" for features that may never be built.

Summary

PrincipleRuleTrap to Avoid
DRYExtract shared knowledgePremature abstraction
WETAllow intentional duplicationCoupling unrelated logic
AHAAbstract after 3 repetitionsHasty abstractions
KISSSimplest solution firstClever one-liners
YAGNIBuild only what's needed todaySpeculative engineering
DIInject dependencies, don't create themField injection

Related Articles

  • Clean Code Principles
  • The SOLID Principles Explained
  • Practical Design Patterns
PreviousPractical Design Patterns for Real-World ApplicationsNextMemento Pattern