A system is easier to change when its boundaries make sense. That sounds obvious until a small product decision requires edits across a database query, a route, a view component, and three unrelated tests.
Give each layer a clear responsibility
A repository retrieves information. A service applies the rules that make that information useful. A page presents the result. The value of this separation is not the number of folders it creates. It is the number of assumptions that can change independently.
interface ArticleRepository {
findPublished(slug: string): Promise<Article | null>;
}
async function readArticle(slug: string) {
const article = await repository.findPublished(slug);
if (!article) return null;
return presentArticle(article);
}Design for the next real change
A boundary earns its place when it protects a likely change. Swapping a development fixture for an API is a useful example: the page should keep receiving the same kind of article, even though the retrieval mechanism has changed.
A good abstraction removes a reason for two things to change together.
Keep the contract small
Return the data the caller actually needs.
Validate unfamiliar data at the edge.
Keep failure behaviour explicit.
Test the boundary with a different implementation.
A small contract is easier to reason about and easier to replace. Start there. Add infrastructure when a measured need gives it a job to do.
