Skip to content
Kumar Chandrachooda
Microservices

The Version-Skew Time Capsule

One DShop service targets a different framework, references a frozen NuGet package, and calls an API that no longer exists in the repo - so its Debug configuration cannot compile, and it only builds in Release. Part 11 of Nine Services and a Message Bus.

By Kumar Chandrachooda 28 Sep 2025 5 min read
One service sealed against a 2018 framework the repo has moved past

Eight of the nine DShop services are the same age. They target the same framework, reference the same shared library the same way, and call the same API. Storage is a fossil. It targets a different framework version, references a frozen NuGet package instead of the shared source, and calls an older shape of the framework's API that no longer exists in the repository. The consequence is the strangest build artifact in the estate: Storage's Debug configuration cannot compile, and the service only builds in Release. This part reads how a single .csproj became a sealed time capsule.

Two configurations, two different framework s

Here is the whole of Storage's project file that matters, and it is worth reading as a document of a moment frozen:

<TargetFramework>netcoreapp2.1</TargetFramework>
<!-- ... -->
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
    <ProjectReference Include="..\..\..\DNC-DShop.Common\src\DShop.Common\DShop.Common.csproj" />
    <ProjectReference Include="..\..\..\DNC-DShop.Messages\src\DShop.Messages\DShop.Messages.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release'">
    <PackageReference Include="DShop.Common" Version="1.0.*" />
    <PackageReference Include="DShop.Messages" Version="1.0.*" />
</ItemGroup>

Three things are already unusual. Storage targets netcoreapp2.1; the other eight services target 2.2. It references DShop.Common version 1.0.*; the other services reference a package the estate names DShop.Common_ at 2.0.5 (yes, with a trailing underscore — a name-squat workaround the companion series unpicks in The Rough Draft of Convey). And it is the only service that references DShop.Messages at all — the shared-contracts package everyone else abandoned. Storage is a service from an earlier version of the estate that never got upgraded when the others moved on.

But the target framework and the version numbers are just the label on the capsule. What is inside is the interesting part.

The API it calls no longer exists

Look at how Storage wires up Mongo and Redis in its Startup:

builder.AddMongoDB();
builder.AddMongoDBRepository<Customer>("Customers");
builder.AddMongoDBRepository<Product>("Products");
services.AddRedisCache();

Now look at what the in-repo DShop.Common — the one Storage's Debug configuration references by project path — actually exposes:

public static void AddMongo(this ContainerBuilder builder) { /* ... */ }
public static void AddMongoRepository<TEntity>(this ContainerBuilder builder, string collectionName) { /* ... */ }
public static IServiceCollection AddRedis(this IServiceCollection services) { /* ... */ }

AddMongo, not AddMongoDB. AddMongoRepository, not AddMongoDBRepository. AddRedis, not AddRedisCache. The repository interface exposes AddAsync, where Storage's handlers call CreateAsync. Storage calls a whole vocabulary of methods that the current in-repo framework renamed out of existence. The framework was refactored — AddMongoDB became AddMongo, CreateAsync became AddAsync — and Storage was never updated to match, because Storage does not actually build against that source.

Trace the consequence through the two configurations:

  • Debug references the local DShop.Common.csproj by path. That project defines AddMongo/AddRepository/AddRedis. Storage calls AddMongoDB/AddMongoDBRepository/AddRedisCache. Those methods are not there. Debug does not compile — it is a configuration that references code its source cannot satisfy.
  • Release references DShop.Common version 1.0.* from a package feed — the frozen 2018 build of the framework, the one that still had AddMongoDB and CreateAsync. Against that ancient package, every call resolves. Release compiles, against a NuGet package that preserves the API the repo has since deleted.

So the only way to build Storage is to reach past the repository entirely and pull a years-old package that remembers the API the source has forgotten. The Debug configuration is not just unused — it is a lie the project file tells, pointing at local source it provably cannot compile against.

Why this is the estate's richest artifact

A lot of version skew is invisible — two services on adjacent framework patches, a transitive dependency a minor version apart. This one is visible because it fossilised the API surface itself. You can read, in one .csproj and one Startup.cs, the exact shape the framework had in 2018 (AddMongoDB, CreateAsync) preserved against the shape it has now (AddMongo, AddAsync), because the frozen package and the live source sit side by side in the same repository and disagree.

It also explains why Storage got away with the stale shared contract from Part 4 — its OrderCreated missing Products and carrying a phantom Number. Storage is pinned to DShop.Messages 1.0.*, the frozen contract package, and reads only the id before re-fetching, so the stale shape never bites. The whole service is internally consistent with 2018. It is the rest of the estate that moved.

To be fair, the dual-configuration trick — ProjectReference in Debug for local iteration, PackageReference in Release for reproducible builds — is a legitimate polyrepo pattern, and the companion series shows the framework using it deliberately elsewhere. In a seventeen-repository estate with no monorepo, this is how you get both a fast inner loop (edit the framework source, F5 the service) and a reproducible CI build (restore a pinned package). The intent is sound. The failure here is not the pattern; it is that the Debug half was never kept in sync, so a technique meant to give you a fast local loop instead gives you a configuration that cannot build. A dual-reference scheme is only as honest as its least-maintained branch.

The pin itself carries a second fossil. The other eight services reference the framework as DShop.Common_ — with a trailing underscore — because the un-suffixed DShop.Common id on the public feed was taken, a name-squat the authors worked around by renaming their own package. Storage predates that workaround and references the plain DShop.Common 1.0.*, which is a different package from the DShop.Common_ 2.0.5 the rest of the estate pulls. So Storage is not merely a version behind — it is on a differently-named package lineage that forked when the naming collision forced the rename. Two services in the same estate say “reference the shared framework” and resolve two unrelated NuGet ids.

Picture a new engineer cloning the repo and running the default build — which is Debug. Eight services compile; Storage does not, with errors about AddMongoDB not existing. Nothing in the repository explains why, because the explanation is “this service builds in a configuration you didn't pick, against a package you don't have cached, preserving an API the source deleted.” A time capsule is only discovered by someone who tries to open it the normal way and finds the lock has changed.

The rule of thumb

  • A frozen package pin freezes an API, not just a version. Storage compiles only against 1.0.* because that package remembers method names the live source deleted. Pinning to a floating-but-old version is how a service quietly stops tracking the framework it claims to use.
  • A build configuration you never run rots silently. Debug referenced local source that stopped compiling and nobody noticed, because Release was the path that shipped. If a configuration is load-bearing, CI must actually build it; an unbuilt configuration is documentation of a build that no longer works.

Storage's stale product read-copy is one of several. Next: Four Products, One Truth — the same product identity, represented four ways across four services, each on its own refresh discipline.