Skip to content
kc@kumarChandrachooda.com:~$ cd /blog/the-route-that-never-existed && read --section="top" 0%
Architecture

The Route That Never Existed

GET /payments returns a 404 in Inflow's own root request file. The reason is not a typo - it is the Payments module exercising exactly the architectural freedom the README advertises, which makes this 404 the best evidence in the repository that the claim is true.

By Kumar Chandrachooda 06 Feb 2026 6 min read
Four paths crossing a boundary, one of them stopping short

Part 2 ended on the one request in Inflow.rest that fails differently from the others. Three of the five root requests return 401 because the file sends no credentials — a documentation gap you can fix by adding a header. The fourth returns 404, and no header will help:

###
GET {{url}}/payments

There is no PaymentsController in Inflow. There never was one. git log --all -- '*PaymentsController*' returns nothing across all six branches and all twelve commits, so this is not a route that was removed; it is a route that was written into the request collection without ever existing in the code.

That sounds like the dullest possible finding — a typo in a sample file. It is not. The reason /payments is missing is the single strongest piece of evidence in the repository that its headline architectural claim is true, and unpicking that is worth an article.

Four modules, ten root paths

Here is every route the application serves, gathered from the eight controllers plus the three endpoints mapped directly in Startup and the shared infrastructure:

Module Root paths
Customers /customers
Users /account, /users
Wallets /wallets, /transfers
Payments /deposits, /deposits/accounts, /withdrawals, /withdrawals/accounts
Saga (none — it has no controllers)
Host /, /modules, /docs

Read down the module column and the naive mental model — one module, one root noun, named after the module — survives exactly one row. Customers gets /customers. Users gets /users and /account, because identity and the current session are different resources. Wallets gets /wallets and /transfers, because a transfer is not a wallet. And Payments gets four paths, none of which is /payments, because there is no payment in the Payments module. There are deposits and there are withdrawals, and they are separate aggregates with separate lifecycles.

The project layout says the same thing before you open a single controller. Inflow.Modules.Payments.Core has exactly two top-level folders of substance:

Payments.Core/
├── DAL/
├── Deposits/
├── Withdrawals/
└── Extensions.cs

No Payments/ folder. No Payment entity. The module is a container for two aggregates that share a database and a bounded context, and the HTTP surface reflects the aggregates rather than the container.

The tidiest confirmation is in PaymentsModule.cs, the module's own manifest:

internal class PaymentsModule : IModule
{
    public string Name { get; } = "Payments";

    public IEnumerable<string> Policies { get; } = new[]
    {
        "deposits", "withdrawals"
    };
    ...
}

The module knows it is called Payments. It also knows that the permissions it needs the host to register are deposits and withdrawals. Compare CustomersModule, whose single policy is customers, and UsersModule, whose single policy is users. In three modules out of four the module name doubles as a resource name by coincidence, and in the fourth it does not — and the fourth is the one the root request file guessed wrong about.

Why this is proof, not sloppiness

The README's second sentence is the claim the whole repository is built to demonstrate:

Each module is an independent vertical slice with its custom architecture.

That is a strong claim, and unlike several claims in that file it is verifiable from the .csproj graph alone. The four business modules ship four genuinely different internal shapes:

Module Projects Shape
Users .Api, .Core Two layers. Core holds entities, commands, queries and the DAL side by side, with no domain/application split
Customers .Api, .Core Two layers, but Core has an internal Domain/{Entities,Repositories,ValueObjects} structure
Payments .Api, .Core, .Shared Three projects; .Core split by aggregate, with an intra-module shared kernel
Wallets .Api, .Core, .Application, .Infrastructure Full onion architecture with real dependency inversion

One host, four architectures, deliberately staged so a reader can see that the module boundary is the unit of autonomy, not the internal layering. It is one of the best-executed ideas in the estate and one of the few simplifications-by-contrast that the README actually records in prose.

Now put the two facts together. Autonomy over internal architecture necessarily includes autonomy over the module's public surface. A module that is free to organise itself around its aggregates is free to name its routes after those aggregates. Payments did exactly that. The 404 in Inflow.rest is what it looks like when someone writes a root-level index of the API from the module list rather than from the route table — which is to say, when someone momentarily forgets the very freedom the file three folders up is advertising.

GET /payments fails because the design claim is real. In a monolith where every module were shaped the same way and named its controller after itself, the request would have worked, and the README's boast would have been empty.

That is a defect and a proof at once, and the distinction matters for how you fix it. The fix is not to add a PaymentsController; adding one to satisfy a request file would invert the whole design. The fix is to delete the line, or replace it with GET /deposits.

The endpoint that would have told you

There is a small tragedy in the timing here. The host maps an endpoint that answers this exact question:

public static void MapModuleInfo(this IEndpointRouteBuilder endpoint)
{
    endpoint.MapGet("modules", context =>
    {
        var moduleInfoProvider = context.RequestServices.GetRequiredService<ModuleInfoProvider>();
        return context.Response.WriteAsJsonAsync(moduleInfoProvider.Modules);
    });
}

GET /modules returns the loaded modules and their policies. Against a default run it tells you, in one response, that a module named Payments exists, that it is loaded, and that its permissions are deposits and withdrawals — everything a reader needs to work out why /payments is not a thing. It requires no authentication and it always works.

The API also serves its OpenAPI document at /swagger/v1/swagger.json and mounts ReDoc at /docs. Note UseSwaggerUI is never called, so the reflex /swagger URL is a 404 too; the browsable documentation lives at /docs under the title “Modular API”.

The README mentions none of these three URLs. /, /modules and /docs are the endpoints in this repository that work perfectly, unauthenticated, on a first run — and they are the endpoints nothing tells you about, while Inflow.rest spends four of its five lines on requests that cannot succeed. A newcomer's first thirty seconds are spent on the broken half of an API whose working half is one directory listing away.

The durable version of this

Strip out the specifics and there is a rule here worth carrying:

A route index generated from your module list is a lie the moment any module owns more than one resource — and if your architecture is any good, one of them will. The module list and the route table are different documents, produced by different authorities, and the only honest way to write the second is to read it off the router.

Every framework in this space gives you a way to do that. ASP.NET Core has EndpointDataSource, injectable anywhere, enumerating every registered endpoint with its pattern and metadata. Twenty lines in Startup would have turned /modules into a live route index that could never drift, in a repository that already maps two custom endpoints by hand and already exposes a module registry. The machinery was there. Nobody pointed it at the request file.

The other four requests in Inflow.rest fail for want of a credential, and getting one turns out to be more interesting than it sounds. Next, register as the admin user — a request the collection captions but never explains.