There are two kinds of developers.
The first kind says:
“Yeah, I can probably get that done by Friday.”
The second kind says:
“Absolutely. No problem. I’ll have it done by tomorrow.”
The first developer has learned from experience.
The second developer has just joined the meeting.
In software development, over-promising and under-delivering is practically a design pattern.
We don’t put it in our architecture diagrams, but it’s there.
Usually somewhere between the API layer and the database.
It Started With a Simple Requirement
Every software disaster begins with someone saying:
“It’s a pretty small change.”
There is no such thing as a small change.
There is only a change whose complexity we haven’t discovered yet.
“Just add a button.”
Sure.
The button needs to:
- Call an API.
- Validate permissions.
- Check business rules.
- Update three tables.
- Send an email.
- Publish an event.
- Update the audit log.
- Work on mobile.
- Work in Internet Explorer.
Okay, maybe not Internet Explorer.
But give it time.
The developer looks at the ticket.
“Yeah, probably a day.”
This is the first promise.
The universe has now started a stopwatch.
The .NET Developer’s Optimism
.NET developers have a particularly dangerous form of optimism.
We look at a requirement and think:public async Task DoTheThingAsync() { // TODO: Implement }
We know exactly how to make an API endpoint.[HttpPost] public async Task<IActionResult> DoTheThing() { return Ok(); }
Five minutes later, we feel unstoppable.
The endpoint exists.
The compiler is happy.
Visual Studio has no red squiggly lines.
The developer thinks:
“This is basically done.”
This is how it begins.
Then Reality Arrives
The first problem is authentication.
Easy.
Except the endpoint needs a different permission.
Then authorization.
Easy.
Except the permission system hasn’t been touched since 2018.
Then validation.
Easy.
Except the business rules are documented in a Word document called:Final_Final_Requirements_v7_REALLY_FINAL.docx
Then the database.
Easy.
Except the stored procedure has 2,700 lines.
Then testing.
Easy.
Except nobody knows how the existing integration tests work.
Then deployment.
Easy.
Except production has a configuration setting that doesn’t exist locally.
Suddenly:1 day estimate ↓ 3 days ↓ 1 week ↓ "We're almost there" ↓ "Just fixing a few things" ↓ "Can we move the deadline?"
“It’s Only a Small API”
This phrase has caused more suffering than most production outages.
Someone asks:
“Can you add an endpoint that returns customer orders?”
You think:[HttpGet("{customerId}/orders")] public async Task<IActionResult> GetOrders(int customerId) { return Ok(await _orderService.GetAsync(customerId)); }
Simple.
Then someone asks:
“Can we filter by date?”
Sure.
Then:
“Can we filter by status?”
Sure.
Then:
“Can we sort it?”
Sure.
Then:
“Can we paginate it?”
Sure.
Then:
“Can we export it to Excel?”
Sure.
Then:
“Can the response be backwards compatible with the old endpoint?”
Sure.
Then:
“Can we make it real-time?”
At this point, the original endpoint has become a distributed systems problem.
The Meeting Estimate
There is a strange ritual in software development.
Someone describes a feature.
Everyone looks at you.
You are expected to produce a number.
Immediately.
You have never seen the code.
You don’t know the database.
You don’t know the deployment process.
You don’t know the dependencies.
You don’t know whether the original developer is still with the company.
But someone asks:
“How long?”
So you say:
“Two days.”
Why?
Because saying:
“I need to investigate the existing architecture, identify dependencies, understand the requirements, validate the data model, and then provide an estimate”
doesn’t sound as productive.
So you say two days.
This is how software estimates are born.
Not from engineering.
From social pressure.
The Estimate Multiplier
Experienced developers eventually develop a secret formula.
Someone says:
“How long will this take?”
Developer thinks:Actual time × 3
Manager hears:2 days
Developer says:
“Probably about a week.”
Manager hears:
“Two days.”
Developer goes back to their desk.
The manager tells the product owner:
“We’ll have it by Wednesday.”
The product owner tells the customer:
“Definitely this week.”
The customer tells their customers:
“You’ll have it Friday.”
Friday arrives.
Nobody has it.
“But You Said It Would Be Done”
Yes.
I also once said I’d understand Kubernetes.
We all make mistakes.
The problem isn’t estimating incorrectly.
The problem is pretending that uncertainty doesn’t exist.
Software contains uncertainty everywhere.
We don’t know:
- What we’ll discover in the existing code.
- What requirements will change.
- What bugs we’ll uncover.
- What external dependencies will break.
- What production configuration differs from development.
- What the database will do under real traffic.
- What security requirements someone forgot to mention.
Yet we frequently give estimates as though we’re manufacturing identical screws in a factory.
The Developer Who Says “No Problem”
Be suspicious of the phrase:
“No problem.”
Especially when it’s said immediately.
Experienced developers tend to say:
“It depends.”
This sounds less impressive.
But “it depends” is often the beginning of an honest engineering conversation.
For example:
“Can we add this feature by Friday?”
A responsible answer might be:
“Probably, assuming the existing API supports it and we don’t need database changes. I’d like to spend a few hours investigating before committing.”
A dangerous answer is:
“Absolutely.”
Because “absolutely” is just a future apology wearing a confident hat.
Async/Await Has Given Us False Confidence
.NET developers have another problem.
We can make almost anything look simple with:await SomethingAsync();
Look at that.
Beautiful.
Asynchronous.
Modern.
Efficient.
We have no idea what SomethingAsync() actually does.
It could be:await SomethingAsync() ↓ HTTP request ↓ Retry policy ↓ OAuth token refresh ↓ Third-party API ↓ Database query ↓ Stored procedure ↓ Linked server ↓ Another database
But our code says:await SomethingAsync();
So obviously it’ll take approximately 14 milliseconds.
Right?
The Magic of Dependency Injection
Dependency injection makes everything look beautifully organized.public OrderService( IOrderRepository repository, IPaymentService paymentService, IEmailSender emailSender, IAuditService auditService, ILogger<OrderService> logger) { }
Look at that constructor.
So clean.
So maintainable.
So enterprise.
The developer confidently says:
“The architecture is flexible.”
Then they try to run the application.Unable to resolve service for type 'IPaymentService' while attempting to activate 'OrderService'.
Ah.
The abstraction is working beautifully.
Nothing has been delivered.
The Sprint Commitment
Sprint planning is where optimism reaches its highest level.
The team looks at 40 tickets.
Someone asks:
“Can we get all of these done?”
Developer:
“If nothing unexpected happens.”
Product owner:
“Great!”
Developer:
“That’s not what I—”
Too late.
It’s committed.
Then someone takes vacation.
Someone else gets pulled into production support.
The CI pipeline breaks.
A security vulnerability appears.
The database migration fails.
The product owner changes the requirement.
And somehow the sprint is still considered unsuccessful because:
“The team didn’t complete the committed work.”
Yes.
Because apparently the sprint was planned in a universe where software behaves predictably.
The Under-Deliver Part
Over-promising is only half the problem.
The other half is what happens when we realize we can’t deliver.
There are two approaches.
Approach One
Disappear into the codebase.
Stop replying to messages.
Change your Slack status to:
“Deep work.”
This means:
“I have no idea what’s happening.”
Approach Two
Communicate early.
Say:
“I underestimated this. I found three dependencies that weren’t apparent initially. The original estimate is no longer realistic. Here’s what I can deliver by Friday, and here’s what needs additional time.”
Approach Two is significantly less exciting.
It is also considerably more professional.
The “Almost Done” Lie
Nothing in software is more dangerous than:
“It’s almost done.”
Almost done can mean anything.
It can mean:Code complete
Or:Code complete Tests missing
Or:Works locally
Or:Works with my test data
Or:Works if you don't click that button
Or:Production deployment hasn't been attempted yet
In reality, “done” should mean something closer to:Implemented + Tested + Reviewed + Integrated + Deployable + Documented where necessary + Accepted
Unfortunately, “almost done” sounds much better in a status meeting.
The .NET Build Is Green, So We’re Done
We’ve all experienced this.
The build succeeds.Build succeeded. 0 Error(s) 0 Warning(s)
Beautiful.
You deploy.
Production immediately throws:InvalidOperationException
Because the build was successful.
It never promised the application would work.
It merely confirmed that the compiler had no objections.
This is an important distinction in software engineering:
Compilation is not correctness.
A green build is not a green production environment.
Technical Debt Loves Optimistic Developers
Every time we say:
“We’ll fix it properly later.”
technical debt quietly opens another account.TODO: Refactor this
Six months later:TODO: Refactor this
Two years later:TODO: Refactor this
The developer who wrote it has left.
The new developer asks:
“Why is this like this?”
Someone answers:
“Legacy.”
Legacy is often what we call our own decisions after we forget why we made them.
How to Stop Over-Promising
The solution isn’t to become the developer who always says:
“No.”
The solution is to become the developer who understands uncertainty.
Instead of:
“I’ll have it done Friday.”
Try:
“I expect to have it done Friday, assuming there aren’t significant changes to the existing implementation.”
Instead of:
“That’s easy.”
Try:
“The change looks straightforward, but I haven’t investigated the existing code yet.”
Instead of:
“Two days.”
Try:
“I estimate one to three days. I’ll know more after I investigate the database and existing API.”
This isn’t weakness.
It’s engineering.
Give Estimates as Ranges
Software estimates often become more useful when expressed as ranges.
Instead of:3 days
say:2–4 days
And explain what drives the uncertainty.
For example:2 days: Implementation is straightforward. 4 days: Existing database logic requires changes or additional testing.
Now the estimate communicates information.
The number isn’t pretending to be a prophecy.
It’s describing uncertainty.
Under-Promise and Over-Deliver?
You’ve probably heard this advice:
“Under-promise and over-deliver.”
It sounds great.
But there is a problem.
If you consistently under-promise, you can also become the person who says:
“This will take three weeks.”
and delivers it in two days.
Eventually management learns:
“Their three-week estimates really mean two days.”
Congratulations.
You have created another estimation problem.
The goal isn’t to manipulate expectations.
The goal is accurate expectations.
Don’t under-promise.
Don’t over-promise.
Communicate honestly about what you know and what you don’t know.
The Most Valuable Developer in the Room
The most valuable developer isn’t necessarily the one who says:
“I can build that.”
It’s often the developer who says:
“Before we commit to that date, there are a few things we should investigate.”
That’s not being difficult.
That’s preventing future disappointment.
They ask:
- What does the existing system do?
- What dependencies are involved?
- What are the unknowns?
- What can we deliver incrementally?
- What could make the estimate wrong?
- What is the minimum useful version?
- What needs to be tested?
- What happens when something fails?
Those questions may slow down the meeting.
They can also prevent a month of misery.
Software Doesn’t Care About Your Deadline
This may be the hardest lesson.
You can put:Friday 5:00 PM
on a project plan.
The database doesn’t care.
The compiler doesn’t care.
The external API doesn’t care.
The production environment definitely doesn’t care.
Reality wins.
The only thing we control is how honestly we communicate with the people depending on us.
Final Thoughts
Over-promising usually doesn’t start with dishonesty.
It starts with optimism.
We want to help.
We want to look capable.
We don’t want to disappoint people.
We don’t want to be the developer who says:
“I don’t know.”
So we say:
“Sure.”
Then we discover that the “simple change” requires modifying an API, three services, two stored procedures, a database migration, an authentication policy, a background worker, and a configuration setting that only exists in production.
Suddenly Friday looks very far away.
The answer isn’t pessimism.
It’s professionalism.
It’s okay to say:
“I don’t know yet.”
It’s okay to say:
“I need to investigate.”
It’s okay to say:
“That estimate has changed.”
It’s okay to say:
“We can deliver part of it by Friday, but the complete feature will take longer.”
And yes, it’s even okay to say:
“This is more complicated than we originally thought.”
Because the goal of software development isn’t to make impressive promises.
It’s to build software that actually works.
Preferably before the customer starts asking where it is.
And preferably without discovering that the entire feature depends on a 4,000-line stored procedure named:usp_GetFinalFinalCustomerData_NEW2
Promise what you reasonably believe you can deliver.
Communicate when reality changes.
And never trust a requirement that begins with, “It should only take five minutes.”