DEV Community

Cover image for The Hidden Cost of Starting Development Too Early
mjodeh
mjodeh

Posted on

The Hidden Cost of Starting Development Too Early

On Monday, a feature moves to In Progress.

By Tuesday, the repository has a new module, a database migration, and a set of endpoints. On Wednesday, the first pull request is open. By Friday, the happy path works in a demo.

The work looks healthy. The board is moving. Code is accumulating.

But the team still has not agreed on what should happen when a request is repeated, a dependency times out, an event arrives out of order, or a user changes state halfway through the operation.

That is not a small planning gap. It means implementation began before the product's behaviour was defined.

Early coding often creates an illusion of progress because it produces visible artifacts: commits, screens, endpoints, and passing tests. At the same time, it quietly converts unresolved decisions into implementation assumptions.

And code is one of the most expensive places to store ambiguity.

A ticket can be detailed and still not be decision-ready

Many tickets describe the shape of a feature without defining its behaviour.

Consider a seemingly simple operation:

POST /subscriptions/{id}/activate

202 Accepted
Enter fullscreen mode Exit fullscreen mode

That contract looks implementable. It is not—not yet.

The endpoint says nothing about the questions that determine the real system:

  • Is activation synchronous, asynchronous, or eventually consistent?
  • Is the operation idempotent? What happens when the client retries after a timeout?
  • Can a subscription be cancelled while payment is pending?
  • If payment.succeeded arrives after cancellation, which transition wins?
  • Are events delivered at most once, at least once, or without an ordering guarantee?
  • Which service owns the authoritative state?
  • What does the caller observe during partial failure?
  • What evidence proves the feature is accepted?

Two experienced engineers can implement the same ticket correctly and still build incompatible systems because each filled in those blanks differently.

The problem is not coding ability. The problem is that a product decision was allowed to masquerade as an implementation detail.

Four agreements should exist before production implementation

A team does not need a hundred-page specification before writing code. It does need enough shared precision to prevent each engineer from inventing a different product.

At minimum, four things should be agreed.

1. Behaviour

Behaviour defines what the system does from the user's or caller's perspective.

For stateful features, prose is rarely enough. Make the transitions explicit:

draft -> pending_payment -> active
                    |          |
                    v          v
                 cancelled   suspended
Enter fullscreen mode Exit fullscreen mode

Then challenge the diagram:

  • Which commands are valid in each state?
  • Which transitions are reversible?
  • What happens on duplicate, late, or out-of-order events?
  • Which outcomes are visible to the user?

If the team cannot answer those questions consistently, the behaviour is not yet ready to encode.

2. Constraints

Constraints are the invariants and operating limits the implementation must preserve.

They include business rules—such as plan limits, permission boundaries, and legal restrictions—but also system properties such as latency budgets, data residency, transaction boundaries, availability targets, and compatibility requirements.

A technically correct implementation can still be unusable if it violates a constraint that surfaced too late. Discovering after development that an operation must be atomic across two stores, complete within 200 milliseconds, or run without transferring data across regions is not refinement. It is redesign.

3. Integration contracts

An integration contract is more than a request and response schema.

It should define ownership, versioning, failure semantics, timeout behaviour, retry policy, idempotency, ordering guarantees, and compatibility expectations. For event-driven systems, it should also answer who may emit an event, what makes it unique, how consumers recover, and how schema evolution is handled.

The happy-path payload is usually the cheapest part of an integration. The expensive part is everything that happens when the network behaves like a network.

4. Acceptance criteria

Acceptance criteria should make success observable and falsifiable.

“The user can activate a subscription” is a summary, not a testable criterion. Stronger criteria describe the initial state, the action, the expected outcome, and the important failure cases.

For example:

Given a subscription is pending payment
And the user has cancelled it
When a delayed payment-success event is received
Then the subscription remains cancelled
And the payment is queued for refund exactly once
And the decision is visible in the audit trail
Enter fullscreen mode Exit fullscreen mode

This single scenario aligns product behaviour, the domain state machine, integration semantics, idempotency, and observability. It removes far more uncertainty than another day of speculative implementation.

Why early coding feels faster

Implementation has a psychological advantage: it is concrete.

A team can count pull requests, story points, endpoints, components, and passing tests. Agreement work produces fewer visible artifacts, even when it removes the uncertainty most likely to delay delivery.

That makes it easy to confuse activity with progress.

Early implementation also tends to move quickly because engineers choose convenient defaults for every unanswered question. The first version may look efficient precisely because it has not yet encountered the decisions it postponed.

Tests can reinforce the illusion. A test suite proves that the code behaves as its authors expected. It does not prove that those expectations were shared, complete, or correct. If the team never agreed on the behaviour, green tests may simply preserve the wrong assumption more rigorously.

The branch is moving. The decision graph is not.

Rework grows across surfaces, not files

The cost of an unresolved decision is not limited to rewriting a function.

A useful engineering heuristic is:

Expected rework ~= unresolved decisions
                  x affected surfaces
                  x cost of reversal
Enter fullscreen mode Exit fullscreen mode

Suppose the team initially treats subscription activation as synchronous, then later agrees that it must be asynchronous and resilient to duplicate delivery. That decision can change:

  • the domain state model;
  • database schema and existing records;
  • public API responses;
  • event schemas and consumer logic;
  • client-side loading and error states;
  • retry and idempotency mechanisms;
  • tests and test data;
  • metrics, alerts, and operational runbooks;
  • analytics definitions and support documentation.

At that point, calling the work “a refactor” understates it. The team is reversing a distributed decision that has already propagated through the system.

This is why a small ambiguity at the start can become a large delivery cost later. The ambiguity stayed constant; its blast radius grew.

The answer is not heavyweight specification

The alternative to premature implementation is not months of analysis or a demand for perfect certainty.

The goal is a decision-ready slice: the smallest set of artifacts that lets engineering start without inventing product behaviour inside the codebase.

For many features, that can be lightweight:

  • a few concrete examples covering the main and failure paths;
  • a state transition sketch for stateful behaviour;
  • explicit invariants and non-functional constraints;
  • versioned request, response, or event schemas;
  • documented timeout, retry, and idempotency semantics;
  • testable acceptance criteria;
  • a short list of remaining unknowns, each with an owner.

These artifacts should be reviewed together, not produced by separate functions and thrown across handoff boundaries. Product, design, engineering, and dependent teams need to see the same behaviour from their respective angles.

The objective is not to eliminate learning. It is to decide which uncertainty is acceptable before the cost of changing it multiplies.

Exploration code and commitment code are different

Sometimes coding early is exactly the right move.

A time-boxed spike can test whether a library supports a required protocol, whether a query can meet the latency budget, or whether an external API behaves as documented. A prototype can expose usability questions faster than a meeting.

But exploratory code needs an explicit contract of its own:

  • What question is it answering?
  • How long will the exploration run?
  • What evidence will it produce?
  • Is the code disposable?
  • What decision must be made before it becomes production implementation?

Without those boundaries, prototypes have a habit of becoming foundations. Temporary assumptions acquire users, data, and dependencies. The team then has to preserve decisions it never consciously made.

Exploration code can start early. Commitment code should wait until the team knows what it is committing to.

A practical start-development gate

Before a feature enters production implementation, I would expect the team to answer these questions:

  1. Can we describe the user-visible outcome in the same words?
  2. Are the important states and transitions explicit, including retries and out-of-order events?
  3. Are business and technical invariants documented?
  4. Do integration contracts define failure semantics, not only payloads?
  5. Are acceptance criteria objective enough to become tests and observable enough to operate in production?
  6. Are the remaining unknowns named, owned, and deliberately accepted?

If the answer is no, the next step is not necessarily “stop.” It may be a short behaviour workshop, a contract review, or a targeted technical spike.

But it should not be production code pretending the decision has already been made.

Shipping sooner starts with deciding sooner

Starting development early can improve a schedule on paper while making the real delivery date less predictable. The first week looks fast because the team is borrowing time from later weeks—when assumptions collide, interfaces change, data must be repaired, and tests have to be rewritten.

High-performing teams do not delay implementation for ceremony. They delay irreversible commitments until the relevant decisions are explicit.

The best time to discover that two teams mean different things by “activated” is before both definitions are running in production.

The gap between intent and implementation—where behaviour, constraints, contracts, and evidence either align or drift—is the problem space I am exploring with Lopeline. I will share more about that soon.

Top comments (0)