Imagine you are asked to build:
A simple blogging platform.
Current requirements:
Create Post
Edit Post
Delete Post
View PostA 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 codehave been written.
The project still doesn't support:
Create Postproperly.
The team spent time building:
Features Nobody Asked ForThis is one of the most common causes of software bloat.
To solve this problem, the Agile community introduced:
YAGNIHistorical background
YAGNI originated in Extreme Programming (commonly called XP)
created by: Kent Beck
The core observation:
Teams were spending enormous effort building:
Potential Featuresinstead of:
Actual FeaturesMost of those potential features:
Were Never UsedThus: 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 WeightExample:
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 ChildInstead of building:
3 BedroomsYou build:
25 Bedrooms
3 Swimming Pools
Helicopter Pad
Movie TheaterWhy?
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 CostEven if nobody uses it.
Unused code is still:
Code You Must MaintainWhy 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.
Leave a comment
Your email address will not be published. Required fields are marked *


