Skip to content
intermediate Phase 8 · SOLID & Design Principles

DRY, KISS & YAGNI Principles

Understanding when to apply and when to avoid DRY, KISS, and YAGNI principles in PHP and Magento development

45m
0 problems
Topic Progress 0%

DRY - Don't Repeat Yourself

The Principle

Every piece of knowledge must have a single, unambiguous representation within a system. Duplication leads to inconsistency when only one copy is updated.

Good DRY: Extracting Price Calculation

// BAD: Duplicated in multiple places
// In DiscountCalculator
$finalPrice = $product->getPrice() * (1 - $discount / 100);
$finalPrice = $finalPrice * (1 + $taxRate);

// In OrderTotal
$finalPrice = $item->getPrice() * (1 - $item->getDiscount() / 100);
$finalPrice = $finalPrice * (1 + $item->getTaxRate());

// GOOD: Single source of truth
namespace Vendor\Catalog\Model\Pricing;

class PriceCalculator
{
    public function calculateFinalPrice(float $price, float $discount, float $taxRate): float
    {
        $discounted = $price * (1 - $discount / 100);
        return $discounted * (1 + $taxRate);
    }
}

// Both classes use PriceCalculator via dependency injection

When DRY Becomes Harmful

// BAD: Forced DRY couples unrelated things
namespace Vendor\Catalog\Model;

class Product implements \Vendor\Sales\Api\HasPriceInterface
{
    // Product price calculation
    public function calculatePrice(float $base, float $tax): float
    {
        return $base * (1 + $tax);
    }
}

// Later, someone reuses this for shipping...
class ShippingCalculator implements \Vendor\Sales\Api\HasPriceInterface
{
    // Shipping uses same method but tax logic differs!
    public function calculatePrice(float $base, float $tax): float
    {
        return $base * (1 + $tax); // Wrong! Shipping tax is different
    }
}

This is called "accidental duplication" - the code looks similar but the requirements diverge. Sharing the implementation creates a hidden coupling.

Rule of Thumb

DRY applies to knowledge, not just code. If two things change for the same reason, they should be unified. If they change for different reasons, keep them separate even if they look identical now.

KISS - Keep It Simple, Stupid

The Principle

Most systems work best if they are kept simple rather than made complicated. Simplicity should be a key goal in design.

Violation: Over-Engineered Validation

namespace Vendor\Checkout\Validator;

interface ValidationStrategyInterface
{
    public function validate(mixed $value): ValidationResultInterface;
}

namespace Vendor\Checkout\Validator\Strategy;

class StringNotEmptyStrategy implements ValidationStrategyInterface
{
    public function __construct(
        private ValidationContextFactory $contextFactory,
        private ValidationResultBuilder $resultBuilder
    ) {}

    public function validate(mixed $value): ValidationResultInterface
    {
        $context = $this->contextFactory->create();
        return $this->resultBuilder
            ->setContext($context)
            ->addRule(new NotEmptyRule())
            ->addRule(new StringTypeRule())
            ->build($value);
    }
}

This is 20 lines of code to check if a string is empty.

KISS Version

namespace Vendor\Checkout\Validator;

class AddressValidator
{
    public function validate(array $data): array
    {
        $errors = [];

        if (empty(trim($data['street'] ?? ''))) {
            $errors[] = 'Street address is required';
        }

        if (empty(trim($data['city'] ?? ''))) {
            $errors[] = 'City is required';
        }

        if (empty($data['postcode'])) {
            $errors[] = 'Postcode is required';
        }

        return $errors;
    }
}

15 lines, immediately readable, easy to modify. The over-engineered version adds cognitive overhead without real benefit.

KISS in Magento Context

// Over-engineered: factory + strategy + builder for simple config
public function getShippingMethod(): string
{
    $factory = $this->methodFactoryRegistry->get($this->config->getMethodType());
    $strategy = $factory->createStrategy();
    return $strategy->resolve();
}

// KISS: direct approach
public function getShippingMethod(): string
{
    return $this->scopeConfig->getValue('shipping/method');
}

If it's just reading config, don't build a framework around it.

YAGNI - You Aren't Gonna Need It

The Principle

Don't implement something until it is actually needed. Every unused feature is dead code that adds maintenance burden.

Violation: Anticipatory Abstraction

namespace Vendor\Catalog\Model;

// "What if we need to support multiple file formats?"
interface ExporterInterface
{
    public function export(array $data): string;
}

interface ImporterInterface
{
    public function import(string $data): array;
}

interface TransformerInterface
{
    public function transform(array $input): array;
}

class CsvExporter implements ExporterInterface { /* ... */ }
class JsonExporter implements ExporterInterface { /* ... */ }
class XmlExporter implements ExporterInterface { /* ... */ }
class CsvImporter implements ImporterInterface { /* ... */ }
class JsonImporter implements ImporterInterface { /* ... */ }
class XmlImporter implements ImporterInterface { /* ... */ }

// Total: 6 classes + 3 interfaces for a feature nobody asked for

YAGNI Version

namespace Vendor\Catalog\Model\Export;

// Build what you actually need right now
class ProductCsvExporter
{
    public function export(): string
    {
        $products = $this->productCollection->load();
        $csv = '';
        foreach ($products as $product) {
            $csv .= implode(',', [
                $product->getSku(),
                $product->getName(),
                $product->getPrice(),
            ]) . "\n";
        }
        return $csv;
    }
}

// When JSON export is actually requested:
// THEN refactor to interface + multiple implementations

The YAGNI Test

Ask yourself:

  1. Will we definitely use this in the next sprint? If not, don't build it.
  2. Is there a concrete user story? If not, it's speculation.
  3. Can we add it later without major refactoring? Usually yes.

When YAGNI Hurts

YAGNI doesn't mean "never plan ahead." If you're building an API and know you'll need versioning, adding a version parameter to routes from day one is wise — it's not YAGNI because you have concrete requirements for it.

The key: distinguish between uncertainty (YAGNI) and known requirements (build it now).

Quiz

1. When does DRY become harmful?

Question 1 options

2. YAGNI advises against:

Question 2 options

3. KISS is best demonstrated by:

Question 3 options

Flashcards

Question

What does DRY stand for?

Answer

Don't Repeat Yourself - single source of knowledge

Question

What does KISS stand for?

Answer

Keep It Simple, Stupid - prefer simple solutions

Question

What does YAGNI stand for?

Answer

You Aren't Gonna Need It - don't build until needed

Question

When is DRY harmful?

Answer

When it couples unrelated things that change for different reasons

Revision Notes

Key Takeaways

  • 1. DRY unifies knowledge that changes for the same reason
  • 2. KISS prioritizes simplicity and readability over cleverness
  • 3. YAGNI prevents premature feature development
  • 4. All three can be harmful when applied dogmatically
  • 5. Balance: apply principles pragmatically based on context

Interview Tips

  • Give examples where following these principles actually hurt a project
  • Discuss the tension between YAGNI and designing for extensibility
  • Know that Magento often errs on the side of flexibility (sometimes violating YAGNI)

Cheat Sheet

DRY  → Eliminate real duplication, not apparent similarity
KISS → Simple > clever; readable > concise
YAGNI → Build it when you need it, not before

Check: Does this change for the same reason? → DRY
Check: Is this the simplest approach? → KISS
Check: Do we have a requirement for this? → YAGNI