The Config Is a Promise the Code Never Made
Trill's appsettings files and package references describe caching, service discovery, health checks, dynamic secret leases and API documentation - and reading the source shows how much of that is never wired up at all.
There is a moment in every estate audit where you stop reading appsettings.json and start reading Program.cs, and the system shrinks. The configuration described a service with distributed caching, service discovery, dynamic database credentials and health checks. The code contains a controller and a Mongo repository. Nobody lied; the settings were written when the feature was planned and never deleted when it was not built.
Part 2 showed four manifests disagreeing about which services exist. This part goes one level down, into a single service, and asks a narrower question: of everything this unit's configuration and package list claims, how much does the code actually call?
Twenty-three packages, nine calls
Trill.Pusher.csproj declares twenty-three Convey package references — plus Grpc.AspNetCore, Grpc.AspNetCore.Web and Microsoft.Extensions.Logging. Here is the entire registration block from Startup.cs:29-39:
services.AddConvey()
.AddHttpClient()
.AddJwt()
.AddJaeger()
.AddPrometheus()
.AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin())
.AddEventHandlers()
.AddInMemoryEventDispatcher()
.AddCommandHandlers()
.AddInMemoryCommandDispatcher()
.Build();
Nine capabilities, out of twenty-three referenced packages. The unused fourteen are not merely unused — most of them arrive with live configuration, which is what makes the estate misleading rather than merely untidy:
| Package | Configuration present | Wired |
|---|---|---|
Convey.Persistence.Redis |
"redis" block in appsettings.json:121-125 and the docker profile |
no |
Convey.Persistence.MongoDB |
— | no |
Convey.MessageBrokers.Outbox (+ .Mongo) |
— | no |
Convey.Discovery.Consul |
"consul": { "enabled": true, ... } in appsettings.docker.json:2-12 |
no |
Convey.LoadBalancing.Fabio |
"fabio": { "enabled": true, ... } in the docker profile |
no |
Convey.WebApi.Swagger |
"swagger" block in the docker profile |
no |
Convey.CQRS.Queries |
— | no — the Pusher defines no queries |
Convey.Security |
"security": { "certificate": { "header": "Certificate" } }, appsettings.json:126-130 |
no |
Two of those rows deserve to be pulled out, because each is an absence with a receipt attached.
Redis is the backplane this service needs, present as a dependency and absent as code. The Pusher fans stories out to connected browsers through an in-process System.Threading.Channels queue (part 9 is about exactly how badly that goes). The correct fix at more than one replica is Redis pub/sub. The package is referenced. The connection string is configured in three environment files. Not one line of the Pusher touches it.
Consul health-checking is configured against an endpoint the Pusher does not have. appsettings.docker.json:9-11 sets "pingEndpoint": "ping", "pingInterval": 3, "removeAfterInterval": 3. Convey maps /ping as part of AddWebApi(), and the Pusher never calls AddWebApi(). Had Consul actually been wired, the Pusher would have deregistered itself three seconds after registering, forever — a flapping-service incident encoded in configuration, waiting for someone to enable the feature that triggers it.
The Redis ledger, corrected by reading
I want to be careful here, because this is the kind of claim that is easy to overstate. Redis appears in the appsettings.json of seven units: the Pusher, the Saga, Ads, Analytics, Stories, Timeline and Users. Six of those call .AddRedis() in their Extensions.cs. And exactly one of them — Timeline — actually opens a connection:
public class RedisStorage : IStorage
{
private readonly IDatabase _database;
public async Task<Paged<Story>> GetTimelineAsync(Guid userId, DateTime? from = null, DateTime? to = null)
{
var storyIds = await _database.SortedSetRangeByScoreAsync(GetTimelineKey(userId), minScore, maxScore);
var storyEntries = await _database.StringGetAsync(storyKeys);
Trill.Services.Timeline/src/.../Redis/RedisStorage.cs:12-28. Timeline is a real, well-shaped Redis fan-out service — sorted sets for timelines, string keys for story bodies, a single multi-get to rehydrate. So the honest statement is not “Redis is unused”; it is that five services register a Redis connection they never open, and one service uses it properly. The five are carrying a live dependency on an infrastructure component for no functional reason, which means Redis going down takes them with it at startup and buys them nothing when it is up.
The rest of the absence ledger
The pattern repeats across the estate, and it is worth seeing the whole list at once, because the shape only becomes obvious in bulk:
- Vault dynamic credentials. All eight
appsettings.jsonfiles configure"lease": { "mongo": { "type": "database", "roleName": "...", "autoRenewal": true, "templates": { "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" } } }. That is short-lived, per-service database credentials with automatic renewal — a genuinely advanced piece of secrets hygiene. It is"enabled": falsein everydevelopmentanddockerprofile, which is to say in every runnable profile. - Swagger on units with no HTTP surface. The Pusher and the Saga both configure a
"swagger"section. Neither registers Swagger. Meanwhile the gateway — the aggregated entry point, the one place a consumer would look for documentation — has no Swagger section and no documentation. GENERATOR_ID. Stories' snowflake ID generator reads it, correctly, to parameterise the machine id. It is set in no Dockerfile, no compose file, no Tye file and no PM2 file. Every replica defaults to generator 0.mongo.seed: falsein every service. There is no seeder in the estate to enable.- Grafana runs in the infrastructure compose file with no dashboards, no datasource provisioning and no volume. It starts empty every time.
- Prometheus sets
evaluation_interval: 5sand declares norule_files:. There is nothing to evaluate and no Alertmanager in either compose file. - Fabio is registered by all five domain services. No service's
httpClient.servicesmap points at Fabio's port 9999; they all hardcodehttp://localhost:50xxorhttp://ads-service.
Twenty-one such items across the estate, by my count.
A load balancer with nothing to balance
The Pusher's base configuration contains this, on appsettings.json:11-14:
"httpClient": {
"type": "fabio",
"retries": 3,
"services": {},
...
}
"type": "fabio" tells Convey's HTTP client to resolve service addresses through Fabio, the Consul-backed load balancer running in the estate's infrastructure compose file. The Pusher does call AddHttpClient(), so that setting is actually bound. It does not call AddFabio(), and its services map is empty, and it makes no outbound HTTP calls at all — the Pusher's only outputs are two gRPC streams.
The same "type": "fabio" appears in the Saga and in all five domain services. Five of them do call .AddConsul().AddFabio(). And not one service's httpClient.services map points at Fabio's port 9999; they all hardcode http://localhost:5030 or http://ads-service. Fabio is registered by five units and used by none, which means the Consul registrations those five services perform serve no routing purpose either.
Notice which four units opt out of Consul entirely: the Pusher, the Saga, the gateway and the Web. The gateway — the one component that would most benefit from resolving its five YARP destinations through service discovery instead of five hardcoded localhost addresses — is one of the four. Discovery covers exactly the units that do not need it.
The option nobody reads
My favourite instance is smaller and sharper than any of those, and it lives in the gateway. MessagingOptions binds the "messaging" configuration section:
internal class MessagingOptions
{
public bool Enabled { get; set; }
public IEnumerable<EndpointOptions> Endpoints { get; set; }
Trill.APIGateway/src/.../Framework/MessagingOptions.cs:5-8. The gateway's appsettings.json:64-66 duly sets "messaging": { "enabled": true, "endpoints": [ ... ] }. And MessagingMiddleware's constructor reads:
_endpoints = messagingOptions.Value.Endpoints?.Any() is true
? messagingOptions.Value.Endpoints.GroupBy(e => e.Method.ToUpperInvariant())
.ToDictionary(e => e.Key, e => e.ToList())
: new Dictionary<string, List<MessagingOptions.EndpointOptions>>();
MessagingMiddleware.cs:35-38. Enabled is bound, never read, and never referenced anywhere else in the repository. Setting "messaging": { "enabled": false } in production would change nothing at all; the HTTP-to-AMQP bridge stays live as long as the endpoint list is non-empty. It is a feature flag that does not flag a feature — three words of YAML that a reader, an operator, and quite possibly the author would all read as a kill switch.
What to take from it
To be fair to the estate, most of this has an innocent explanation and it is the same one every time: Convey is a service chassis, and a chassis is designed to be enabled by configuration. Adding Convey.Persistence.Redis and a "redis" block is exactly how you would begin to use Redis, and stopping halfway leaves no visible failure. The estate's Extensions.cs-per-service convention is genuinely excellent for legibility — one fluent chain in one file tells you the unit's whole capability set — but it also means the gap between the chain and the configuration is the only place the truth lives, and nothing surfaces it.
Which is the durable rule this estate teaches by omission: configuration is a claim, and the only artefact entitled to make claims about a system's capabilities is a call site. If you audit an unfamiliar service, start from ConfigureServices, not appsettings.json. If you own one, delete the config block the same day you decide not to build the feature — a stale setting is worse than a missing one, because it will eventually be believed.
There is one configured capability that is not merely absent but actively cancelled somewhere else, and it is the estate's most consequential. Next: durable queues on an ephemeral disk.