Nguyen Le PhongNguyen Le Phong

Designing Interfaces That Outlive Implementations

A practical reflection on designing software interfaces that keep product behavior stable while implementations change underneath.

The design discussion started with a small rectangle on the whiteboard. Inside it, someone had drawn a boundary between two parts of the system. The code behind that boundary was still messy. The database table would probably change. The product name might change too. But the team kept returning to one question: what promise should this part of the system make to the rest of the codebase?

Engineers calmly review interface boundary cards and an abstract architecture diagram around a shared office table.
A durable interface is less about clever abstraction and more about a promise the team can keep while the inside keeps changing.

That is the quiet value of a good interface. It gives a codebase a stable sentence to say while the implementation underneath keeps learning. Storage moves from one table to another. A provider is replaced. A performance shortcut is removed. A temporary branch becomes a proper workflow. If every caller depends on the internal shape, each change becomes a negotiation with the whole system. If callers depend on a clear promise, the inside can improve without asking every neighbor for permission.

This does not mean interfaces should be designed in a vacuum. The most useful boundaries usually come from watching real changes happen. Which behavior keeps showing up in planning? Which rule do people explain again and again? Which detail keeps leaking into screens, jobs, and tests that should not care? Those leaks are often better design material than a blank architecture diagram.

A durable interface is also not necessarily a large one. Sometimes the best contract is small because it names one job clearly. Sometimes it needs to carry a little more context because hiding that context would make every caller guess. The question is not whether the interface is minimal in an abstract sense. The question is whether it protects the caller from details that are likely to change, while still making the important behavior visible.

Tests help the team keep this promise honest. A test around an interface should not only prove that the current implementation works. It should describe the behavior future implementations must preserve. When the test speaks in product language instead of storage language, the team gets a small guardrail against accidental rewrites of meaning.

There is a human side here too. A stable interface reduces the amount of history a teammate must memorize before making a safe change. They do not need to know every reason the first implementation looked the way it did. They need to know what the boundary promises, what it refuses to promise, and which examples show the edge cases. That kind of clarity is a gift to people who join later.

The difficult part is accepting that an interface is a responsibility, not decoration. Once other parts of the system trust it, changing it carelessly creates cost for everyone else. That is why versioning, deprecation, migration notes, and boring examples matter. They let the system move without pretending that all movement is free.

When an implementation changes but the interface still feels calm, the team has created a small piece of architecture that can survive growth. The code inside may be replaced many times. The promise remains readable. If you have seen one boundary make future changes less tense, I would be glad to hear what made that promise clear.

What did you think?