CLOSE
Updated on 06 Jul, 202632 mins read 128 views

Imagine you are asked to build:

A simple blogging platform.

Current requirements:

Create Post
Edit Post
Delete Post
View Post

A developer starts thinking:

What if someday we support:
- Multiple languages?
- AI Content generation?
- Plugin Marketplace?
- Offline sync?
- Distributed storage?
- Blockchain verification?

Months later:

10,000 lines of code

have been written.

The project still doesn't support:

Create Post

properly.

The team spent time building:

Features Nobody Asked For

This is one of the most common causes of software bloat.

To solve this problem, the Agile community introduced:

YAGNI

Historical background

YAGNI originated in Extreme Programming (commonly called XP)

created by: Kent Beck

The core observation:

Teams were spending enormous effort building:

Potential Features

instead of:

Actual Features

Most of those potential features:

Were Never Used

Thus: YAGNI

Official Definition

Always implement things when you actually need them, never when you just foresee that you may need them.

Simple version:

Don't build for imaginary requirements.

Don't build for tomorrow. Build for today.

The Fundamental Problem

Developers are often afraid of future change.

They think:

Let's build flexibility now.

Examples:

Maybe we'll support 50 databases.

Maybe we'll support 20 payment providers.

Maybe we'll support 100 notification channels.

Reality:

One database.
One payment provider.
One notification channel.

for years.

The extra architecture becomes:

Dead Weight

Example:

Suppose you are working on a project that involves uploading user profile pictures. You current requirements is simple:

  • Accept an image
  • Resize it to 300x300
  • Store it on the local filesystem

Three steps. Straightforward. But then you start thinking ahead.

  • What if we need video uploads later? Better add a media handler interface.
  • What if we switch to cloud storage? Better add a storage provider abstraction.
  • What if users want 3D avatars? Better make the system extensible.
  • What if other teams want to plug in their own handlers? Better build a plugin system.

So instead of writing a simple image uploader, you build something like this:

The Overengineered Version (YAGNI Violation)

// Interface for handling different media types
class IMediaHandler {
public:
    virtual ~IMediaHandler() = default;
    virtual bool canHandle(const std::string& fileType) = 0;
    virtual File process(const File& file) = 0;
};

// Interface for storage providers
class IStorageProvider {
public:
    virtual ~IStorageProvider() = default;
    virtual void store(const File& file, const std::string& path) = 0;
    virtual File retrieve(const std::string& path) = 0;
    virtual void remove(const std::string& path) = 0;
};

// Factory for creating media handlers
class MediaHandlerFactory {
private:
    std::unordered_map<std::string, IMediaHandler*> handlers;

public:
    void registerHandler(const std::string& type, IMediaHandler* handler) {
        handlers[type] = handler;
    }

    IMediaHandler* getHandler(const std::string& fileType) {
        auto it = handlers.find(fileType);
        if (it == handlers.end()) {
            throw std::runtime_error(
                "No handler for type: " + fileType
            );
        }
        return it->second;
    }
};

// Cloud storage adapter (not needed yet)
class CloudStorageAdapter : public IStorageProvider {
private:
    std::string bucketName;
    std::string region;

public:
    CloudStorageAdapter(const std::string& bucketName,
                        const std::string& region)
        : bucketName(bucketName), region(region) {}

    void store(const File& file, const std::string& path) override {
        // Cloud upload logic - not implemented, not needed
    }

    File retrieve(const std::string& path) override {
        // Cloud download logic - not implemented, not needed
        return File();
    }

    void remove(const std::string& path) override {
        // Cloud delete logic - not implemented, not needed
    }
};

// Image handler (the only one actually needed)
class ImageMediaHandler : public IMediaHandler {
public:
    bool canHandle(const std::string& fileType) override {
        return fileType == "image";
    }

    File process(const File& file) override {
        return resize(file, 300, 300);
    }

private:
    File resize(const File& file, int width, int height) {
        // actual resize implementation
        return file;
    }
};

// The bloated engine that ties it all together
class MediaProcessingEngine {
private:
    MediaHandlerFactory* handlerFactory;
    IStorageProvider* storageProvider;

public:
    MediaProcessingEngine(MediaHandlerFactory* handlerFactory,
                          IStorageProvider* storageProvider)
        : handlerFactory(handlerFactory),
          storageProvider(storageProvider) {}

    void upload(const File& file, const std::string& fileType,
                const std::string& path) {
        IMediaHandler* handler = handlerFactory->getHandler(fileType);
        File processed = handler->process(file);
        storageProvider->store(processed, path);
    }
};

Look at what happened. The requirement was three lines of real work: accept, resize, store. But we created 6 classes and interfaces to do it. The CloudStorageAdapter has empty method bodies. The MediaHandlerFactory manages exactly one handler. The IStorageProvider interface has a retrieve and delete method that nothing calls.

We've written dozens of lines of infrastructure code that serves no user and solves no problem.

The Simple Version (YAGNI Applied)

class ImageUploader {
private:
    ImageResizer* resizer;
    LocalStorage* storage;

public:
    ImageUploader(ImageResizer* resizer, LocalStorage* storage)
        : resizer(resizer), storage(storage) {}

    void upload(const File& imageFile) {
        File resized = resizer->resize(imageFile, 300, 300);
        storage->save(resized);
    }
};

This code:

  • Meets today's requirements completely.
  • Is easy to read, test, and debug
  • Can be extended later when a real need arises
  • Has no dead code, no empty method stubs, no speculative abstractions

If the requirement to support cloud storage or video formats arises tomorrow, that's the time to refactor and extend. Not before.

Real-World Analogy

Imagine building a house.

Current family:

2 Adults
1 Child

Instead of building:

3 Bedrooms

You build:

25 Bedrooms
3 Swimming Pools
Helicopter Pad
Movie Theater

Why?

Maybe someday…

This is exactly how many software systems are built.

YAGNI says:

Build the third bedroom first.

The Cost of Unused Features

Many engineers underestimate this.

Unused code is not free.

Every feature creates:

Implementation Cost
Testing Cost
Documentation Cost
Maintenance Cost
Bug Cost
Refactoring Cost

Even if nobody uses it.

Unused code is still:

Code You Must Maintain

Why Premature Work Is Harmful

You might think, “What’s the harm in being prepared?” The truth is, a lot. Speculative code comes with hidden costs that grow over time.

1. Wasted time and effort

Building features that aren’t needed yet wastes development time, review time, and testing time. In the image upload example, creating things like CloudStorageAdapter or MediaHandlerFactory before any real user need means effort is spent on features that may never be used.

2. More complexity

Extra flexibility adds unnecessary layers, making the code harder to understand and maintain. New developers may assume the complexity is important and avoid simplifying it, which makes the design harder to clean up later.

3. Delayed value

Working on “future” features slows down the delivery of real, current needs. If a simple solution could be shipped quickly but an overengineered one takes days, users wait longer for no real benefit.

4. Higher maintenance cost

Even unused code must be maintained. It can introduce bugs, require updates, and complicate changes. For example, even if a feature isn’t used, it still needs updates when dependencies change. Unused code still creates real overhead.

When to Bend the Rule

Like any principle, YAGNI has exceptions. Sometimes planning ahead makes sense. The key is to separate real requirements from guesswork.

Security and compliance

If you’re building systems that handle sensitive data like financial records, medical information, or personal data, you often need things like encryption, audit logs, and access control from the start. These are not “maybe later” features—they are required by law or policy.

Known long-term system constraints

Sometimes requirements are clear from day one, such as high availability, strict uptime guarantees, or multi-region support. These architectural needs are expensive to add later, so they should be designed early if they are already known.

Reusable libraries or frameworks

If you are building something that other teams will depend on, you need to think more carefully about the API design. However, it’s still best to start small and evolve the design based on real usage rather than overengineering upfront.

Key idea

These exceptions are valid only when the need is clear and certain, not hypothetical. You’re not guessing what might be needed—you’re responding to requirements that already exist.

Buy Me A Coffee

Leave a comment

Your email address will not be published. Required fields are marked *

Your experience on this site will be improved by allowing cookies Cookie Policy