<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Odinaka Robert Nnamani</title>
    <description>The latest articles on DEV Community by Odinaka Robert Nnamani (@robertpaschal).</description>
    <link>https://dev.to/robertpaschal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2167197%2Fecea0dc6-e64a-4d17-86da-0ec53a674663.jpeg</url>
      <title>DEV Community: Odinaka Robert Nnamani</title>
      <link>https://dev.to/robertpaschal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robertpaschal"/>
    <language>en</language>
    <item>
      <title>Debugging a distributed mystery</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:02:04 +0000</pubDate>
      <link>https://dev.to/robertpaschal/debugging-a-distributed-mystery-1jj0</link>
      <guid>https://dev.to/robertpaschal/debugging-a-distributed-mystery-1jj0</guid>
      <description>&lt;p&gt;In a monolith, a bug lives in one place. You open the logs, you find it.&lt;/p&gt;

&lt;p&gt;Microservices change the problem. A single user action can cross 5+ services — and the failure can sit in any one of them, or in the gap between two of them.&lt;/p&gt;

&lt;p&gt;Take a signup:&lt;/p&gt;

&lt;p&gt;Auth service → User service → Notification service → Email provider&lt;/p&gt;

&lt;p&gt;Say it fails halfway. The user exists, but the welcome never arrives. Or the account never gets created, but the notification already went out. Where do you look?&lt;/p&gt;

&lt;p&gt;Without centralised logging and tracing, you open four dashboards, four log files, four different formats — and you guess. Each team can honestly say "it worked on my side," because each side only saw its own hop.&lt;/p&gt;

&lt;p&gt;The fix is not more logs. It is one thread through all of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One request ID, carried across every hop, from the first request to the last.&lt;/li&gt;
&lt;li&gt;Centralised logs, searchable by that ID — so one search shows the whole journey, not one service's slice of it.&lt;/li&gt;
&lt;li&gt;Tracing that shows the order and timing of each hop — so you can see where the chain broke, not just that it broke.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With that, debugging stops being a mystery. You follow the request the way the user experienced it: Auth, then User, then Notification, then the provider — and the hop where the thread goes quiet is where you look.&lt;/p&gt;

&lt;p&gt;The rule I keep: no service starts work without the request ID. A log line you cannot tie back to a request is a dead end, and a system full of dead ends is a system you cannot debug.&lt;/p&gt;

&lt;p&gt;Microservices do not make bugs harder to write. They make evidence harder to follow. Centralise the evidence, and the mystery goes away.&lt;/p&gt;

&lt;p&gt;Next in this series: 7 backend lessons I learned the hard way. I write about backend systems that look small until they aren't — follow along if that's your world; the longer pieces live on my Substack.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>debugging</category>
      <category>microservices</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>All DB access lives in one place</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Mon, 26 Oct 2026 12:40:54 +0000</pubDate>
      <link>https://dev.to/robertpaschal/all-db-access-lives-in-one-place-4dj3</link>
      <guid>https://dev.to/robertpaschal/all-db-access-lives-in-one-place-4dj3</guid>
      <description>&lt;p&gt;One mistake I see in backend codebases:&lt;/p&gt;

&lt;p&gt;ORM queries scattered everywhere.&lt;/p&gt;

&lt;p&gt;Controllers query the DB. Services query the DB. Helpers query the DB. The little cron job queries the DB. Change one column and you're grep-ing the whole repo, praying you found every query that touches it.&lt;/p&gt;

&lt;p&gt;Soon your database logic is impossible to track — and nobody can tell you where a value actually gets read or written.&lt;/p&gt;

&lt;p&gt;A better pattern: the repository layer.&lt;/p&gt;

&lt;p&gt;All DB access lives in one place. Controllers ask the repository; they never touch the ORM. Schema knowledge — joins, filters, transaction boundaries — lives in one module per entity. The rest of the codebase just calls methods with names that say what they mean: findActiveUsers(), not db.select().where(...).&lt;/p&gt;

&lt;p&gt;Three things this buys you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cleaner code — business logic stops drowning in query syntax.&lt;/li&gt;
&lt;li&gt;Safer schema changes — rename a column in one file, not seventeen.&lt;/li&gt;
&lt;li&gt;A clean test seam — the boundary you need to mock is obvious.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rule I keep: no raw ORM calls outside the repository. No exceptions for "it's just a small helper." Break the rule once and it's no longer a rule.&lt;/p&gt;

&lt;p&gt;All DB access lives in one place. That's the whole point.&lt;/p&gt;

&lt;p&gt;Next in this series: debugging a distributed mystery. I write about backend systems that look small until they aren't — follow along if that's your world; the longer pieces live on my Substack.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Code evolves quickly, databases remember everything</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:01:19 +0000</pubDate>
      <link>https://dev.to/robertpaschal/code-evolves-quickly-databases-remember-everything-3g4e</link>
      <guid>https://dev.to/robertpaschal/code-evolves-quickly-databases-remember-everything-3g4e</guid>
      <description>&lt;p&gt;A common backend mistake: treating database schemas like temporary code.&lt;/p&gt;

&lt;p&gt;Code deploys in minutes. You ship it, watch it, roll it back if it's wrong. A schema doesn't work like that. The moment production data exists, the schema becomes the memory of the system — and memory doesn't roll back.&lt;/p&gt;

&lt;p&gt;Every shortcut compounds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The column you added as nullable "for now" still has three meanings two years later&lt;/li&gt;
&lt;li&gt;The status value you retired still sits in a million old rows&lt;/li&gt;
&lt;li&gt;The constraint you skipped in the app gets skipped by the second service too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So schema design has to consider what code design can postpone: relationships, constraints, future queries, data integrity. Put the constraint in the database, not just the validation layer — applications change, get rewritten, multiply. The database is the last writer standing, and the only one that sees every row.&lt;/p&gt;

&lt;p&gt;And migrations on live tables are production events, not chores: expand, backfill, dual-write, contract — in that order, over days, not in one deploy.&lt;/p&gt;

&lt;p&gt;Code evolves quickly. Databases remember everything. Design like it.&lt;/p&gt;

&lt;p&gt;Next in this series: all DB access lives in one place. I write about backend systems that look small until they aren't — follow along if that's your world; the longer pieces live on my Substack.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>One action, multiple reactions</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Sat, 24 Oct 2026 12:01:43 +0000</pubDate>
      <link>https://dev.to/robertpaschal/one-action-multiple-reactions-4one</link>
      <guid>https://dev.to/robertpaschal/one-action-multiple-reactions-4one</guid>
      <description>&lt;p&gt;Building event-driven systems changed how I think about software.&lt;/p&gt;

&lt;p&gt;Services shouldn't call each other constantly. They should react to events.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;User signs up → Auth emits an event&lt;br&gt;
→ Notification service sends the welcome email&lt;br&gt;
→ Analytics service records the signup&lt;br&gt;
→ User service continues onboarding&lt;/p&gt;

&lt;p&gt;One action. Multiple reactions.&lt;/p&gt;

&lt;p&gt;The biggest shift is mental: you stop designing request chains and start designing system reactions. The signup service doesn't need to know who cares that a user signed up — it publishes the fact, and the system reacts.&lt;/p&gt;

&lt;p&gt;The honest cost: debugging gets harder. A request chain fails in one place, with a stack trace. An event fans out, and the failure can hide inside any of the reactions — so observability stops being optional, and patterns like the transactional outbox (write the event in the same transaction as the state change, so the two can never disagree) stop being nice-to-haves.&lt;/p&gt;

&lt;p&gt;I run an email platform built this way: a send is one action, and delivery, bounces, complaints and suppression are all reactions to provider events. It's the only shape I've found that scales without the services strangling each other.&lt;/p&gt;

&lt;p&gt;Next in this series: code evolves quickly, databases remember everything. I write about backend systems that look small until they aren't — follow along if that's your world; the longer pieces live on my Substack.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Auth systems aren't features, they're mini platforms</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Fri, 23 Oct 2026 12:02:24 +0000</pubDate>
      <link>https://dev.to/robertpaschal/auth-systems-arent-features-theyre-mini-platforms-2ek4</link>
      <guid>https://dev.to/robertpaschal/auth-systems-arent-features-theyre-mini-platforms-2ek4</guid>
      <description>&lt;p&gt;Authentication systems are more complex than most apps expect.&lt;/p&gt;

&lt;p&gt;Signup alone involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email verification&lt;/li&gt;
&lt;li&gt;OTP storage&lt;/li&gt;
&lt;li&gt;Redis for temporary tokens&lt;/li&gt;
&lt;li&gt;Event emission for notifications&lt;/li&gt;
&lt;li&gt;Audit logging&lt;/li&gt;
&lt;li&gt;User service onboarding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's just creating an account.&lt;/p&gt;

&lt;p&gt;Auth systems aren't features. They're mini platforms.&lt;/p&gt;

&lt;p&gt;Login is the smallest part of one. Underneath it sits identity proofing, session lifecycle, token storage, rate limiting on credential attempts, and an audit trail you can actually reconstruct an incident from — user agent, IP, every credential attempt, logged in a shape fraud detection can use later.&lt;/p&gt;

&lt;p&gt;Most teams scope auth as a ticket. Then they rebuild it three times: once when the first fraud wave hits, once when a customer asks for the audit log, and once when sessions have to work across services.&lt;/p&gt;

&lt;p&gt;I write about the backend systems that look small until they aren't — email infra, retries, identity. Follow along if that's your world; the longer pieces live on my Substack (link in bio).&lt;/p&gt;

</description>
      <category>backend</category>
      <category>webdev</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>One tenant's bad list is spending your SES reputation</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Thu, 22 Oct 2026 12:00:08 +0000</pubDate>
      <link>https://dev.to/robertpaschal/one-tenants-bad-list-is-spending-your-ses-reputation-480j</link>
      <guid>https://dev.to/robertpaschal/one-tenants-bad-list-is-spending-your-ses-reputation-480j</guid>
      <description>&lt;h1&gt;
  
  
  One tenant's bad list is spending your SES reputation
&lt;/h1&gt;

&lt;p&gt;If you run a multi-tenant SaaS on Amazon SES, you probably have one SES account, one or two verified identities, and every tenant's email flowing through them.&lt;/p&gt;

&lt;p&gt;That means you also have exactly one sender reputation. And every tenant you onboard gets to spend it.&lt;/p&gt;

&lt;p&gt;Here's the scenario that should worry you. A tenant imports a purchased list — or an old one, scraped from a CRM export nobody cleaned. They send a campaign. Hard bounces spike. A few recipients mark it as spam. SES watches two numbers across your &lt;em&gt;whole account&lt;/em&gt;: bounce rate and complaint rate. Drift toward a 5% bounce rate or a 0.1% complaint rate and your account goes under review. Push further and SES pauses your sending.&lt;/p&gt;

&lt;p&gt;Not that tenant's sending. &lt;em&gt;Your&lt;/em&gt; sending. Every tenant. The good ones — the ones with clean lists and double opt-ins — wake up to the same dead deliverability as the one who caused it. Their password resets land in spam. Their invoices never arrive. And your dashboard, if it only tracks account-level numbers, tells you something is wrong long after all your tenants already know.&lt;/p&gt;

&lt;p&gt;This isn't a monitoring problem you can alert your way out of. It's an architecture problem. Reputation on SES is pooled by default, so isolation has to be something you build — in three separate places.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Give each tenant its own sending identity
&lt;/h2&gt;

&lt;p&gt;Reputation signals attach to identities: domains, MAIL FROM domains, IP pools. If all your tenants send from one domain on the shared IP pool, there is nothing for those signals to segment &lt;em&gt;by&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What I build instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A sending subdomain per tenant&lt;/strong&gt; — &lt;code&gt;mail.tenant.yourdomain.com&lt;/code&gt;, or the tenant's own domain once they're serious — with its own SPF/DKIM alignment. A tenant who destroys their subdomain's standing has hurt &lt;em&gt;their&lt;/em&gt; subdomain, not your root domain and not the tenant next door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A configuration set per tenant.&lt;/strong&gt; This is the unglamorous piece everything else hangs off. Configuration sets are how you tag a tenant's sends so every bounce, complaint, and delivery event comes back labelled with which tenant produced it. One shared configuration set means unattributed events, and unattributed events mean you're back to guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated IP pools only for tenants who earn them.&lt;/strong&gt; SES lets you assign configuration sets to dedicated IP pools. A tenant with steady, real volume can graduate to their own pool, fully out of the shared blast radius. Tenants without consistent volume stay on shared IPs — a cold dedicated IP with sporadic sends is its own deliverability problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Keep a per-tenant ledger, not an account-level blob
&lt;/h2&gt;

&lt;p&gt;SES hands you an account-level suppression list and a stream of events. Most teams wire the events to an SNS topic, glance at a dashboard, and move on.&lt;/p&gt;

&lt;p&gt;Split the stream by tenant (that's what the per-tenant configuration sets are for) and persist it as a ledger: every send, bounce, complaint, and suppression, recorded per tenant, append-only. Two things fall out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Suppression becomes per-tenant too.&lt;/strong&gt; An address that hard-bounced for Tenant A gets suppressed for Tenant A. On a shared account-level list, one tenant's typo'd import can silently block addresses your other tenants legitimately need to reach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You get a reputation balance per tenant.&lt;/strong&gt; Bounce rate, complaint rate, suppression growth — per tenant, over time. Not "the account looks bad this week," but "this tenant has been decaying for six days." That's the difference between an incident and a conversation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Gate sending on per-tenant outcomes
&lt;/h2&gt;

&lt;p&gt;Isolation without enforcement is just better observability of your own outage. The point of measuring per tenant is that you can &lt;em&gt;act&lt;/em&gt; per tenant.&lt;/p&gt;

&lt;p&gt;The pattern I use — and the one I keep coming back to when I talk to other builders about this — is an outcome gate per tenant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thresholds per tenant.&lt;/strong&gt; When a tenant's bounce or complaint rate crosses your line, the gate pauses &lt;em&gt;that tenant's&lt;/em&gt; sending automatically. The account stays healthy, the other tenants never notice, and the paused tenant gets a specific reason instead of your whole platform getting an SES review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A ramp for new tenants.&lt;/strong&gt; Nobody sends at full volume on day one. New tenants start on a paced schedule with a daily ceiling that rises as their outcome history proves out. A bad list then burns a small, contained allowance — not your account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List hygiene before the first campaign, not after.&lt;/strong&gt; The gate is the last line of defence. Checking an import for the obvious rot before it's allowed to send is the first one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I sketched this gate design out with another builder in a GitHub thread recently — he'd been running deliverability as one global dashboard and treating every incident as platform-wide. He folded the per-tenant gate into his product's roadmap the same week and told me it filled a real gap in his design. It usually does. Most multi-tenant stacks built on SES have exactly this hole; they just haven't had the tenant yet who finds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check in your stack today
&lt;/h2&gt;

&lt;p&gt;You don't need a rebuild to find out how exposed you are. Four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If one tenant spiked your bounce rate tonight, could you name them from your own data tomorrow morning — or only that "the account" had a bad day?&lt;/li&gt;
&lt;li&gt;Do your bounce and complaint events arrive tagged by tenant, or in one undifferentiated stream?&lt;/li&gt;
&lt;li&gt;Is suppression enforced per tenant, or is one tenant's bad import blocking addresses for everyone?&lt;/li&gt;
&lt;li&gt;What, mechanically, stops a tenant from sending their worst list at full volume right now? If the answer is "we'd notice," you don't have a control — you have a hope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can't answer those cleanly, your tenants are sharing more than an SES account. They're sharing a reputation, a suppression list, and an outage waiting for its trigger.&lt;/p&gt;

&lt;p&gt;I'm building these three pieces — per-tenant identities, per-tenant ledgers, per-tenant outcome gates — into rootmail, an email infrastructure layer on SES, and the beta is open if you want to see the approach in practice: &lt;a href="https://rootmail.io/beta" rel="noopener noreferrer"&gt;https://rootmail.io/beta&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>email</category>
      <category>saas</category>
      <category>backend</category>
    </item>
    <item>
      <title>Your microservices aren’t decoupled</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:42:43 +0000</pubDate>
      <link>https://dev.to/robertpaschal/your-microservices-arent-decoupled-4ehc</link>
      <guid>https://dev.to/robertpaschal/your-microservices-arent-decoupled-4ehc</guid>
      <description>&lt;p&gt;Splitting a monolith into services gives you the vocabulary for free: independent, decoupled, autonomous. The architecture has to earn those words — and most don’t.&lt;/p&gt;

&lt;p&gt;There’s a fast way to tell whether yours did. Don’t look at the deployment diagram. Look at where the contracts live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup almost everyone starts with
&lt;/h2&gt;

&lt;p&gt;In a gRPC system, the contracts are the .proto files: the message shapes and service definitions every service agrees to speak. They have to live somewhere, and somewhere usually means "inside whichever service was built first."&lt;/p&gt;

&lt;p&gt;So the orders service owns the protos. The users service needs them, so it imports them straight from the orders repo. Payments needs the same messages, and rather than set up the import, someone copy-pastes the files and moves on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orders-service/          ← .proto files live here
├── users-service        imports them from orders
└── payments-service     copy-pasted them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It ships. And it quietly wires every service to every other service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden coupling
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coordinated deployments.&lt;/strong&gt; The moment a contract lives inside one service, changing it stops being one team’s change. Rename a field, add a required field, and every service importing that proto has to move with you. Your "independent" services now deploy in lockstep — the exact thing microservices were supposed to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking change chaos.&lt;/strong&gt; A breaking change in the proto doesn’t break one build. It breaks every consumer, including the ones you forgot existed. The blast radius of a contract change becomes the whole system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version mismatch bugs.&lt;/strong&gt; The copy-paste in payments drifts. It always drifts. Now two services disagree about the contract while both believe they’re current — and the mismatch doesn’t show up in CI, because each service compiles fine against its own copy. It shows up in production, as garbled payloads and errors nobody can reproduce locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tight inter-service dependency.&lt;/strong&gt; Underneath all of it: services are depending on each other’s code. Not on an interface — on an implementation, owned by another team, on another team’s release schedule.&lt;/p&gt;

&lt;p&gt;This is not independence. It’s a monolith that pays network latency for its coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: contracts are a product, not a folder
&lt;/h2&gt;

&lt;p&gt;Contracts get their own repo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users-service    ──┐
orders-service   ──┼──►  contracts repo (versioned, published)
payments-service ──┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Version it.&lt;/strong&gt; The contracts repo releases like a library: v1.4.2, with breaking changes forcing a new major version instead of ambushing consumers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publish it.&lt;/strong&gt; Each service pulls the contracts as a versioned package — the same way it pulls any other dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consume it.&lt;/strong&gt; Every service pins the version it was built against and upgrades deliberately, on its own schedule, after its own tests pass. A team that needs the new field upgrades this sprint; a team that doesn’t stays on the old version until it’s ready.&lt;/p&gt;

&lt;p&gt;And the hard rule that makes it real: &lt;strong&gt;no service imports another service’s code.&lt;/strong&gt; Not the protos, not a shared "utils" package that quietly becomes a coupling channel. Services meet at the contract, nowhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API-first architecture.&lt;/strong&gt; The contract becomes the designed artifact — reviewed, versioned, and intentional — instead of a byproduct of whichever service happened to be written first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent deployments.&lt;/strong&gt; A contract change ships as a new version, not a coordinated release. Teams deploy when they’re ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controlled breaking changes.&lt;/strong&gt; Breaking changes still happen; now they’re explicit, versioned, and adopted consumer by consumer instead of detonating everywhere at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong service boundaries.&lt;/strong&gt; When the only shared thing is a published contract, the boundary between services is real — enforced by the dependency graph, not by convention and good intentions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;Services depend on abstractions. Not implementations.&lt;/p&gt;

&lt;p&gt;It’s an old rule, and gRPC doesn’t exempt you from it — it just changes what the abstraction is. In a gRPC system, your .proto files are the abstraction. Everything else follows from where you put them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the audit
&lt;/h2&gt;

&lt;p&gt;Three questions, answerable in five minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does one service "own" your proto files?&lt;/li&gt;
&lt;li&gt;Do services import code from each other?&lt;/li&gt;
&lt;li&gt;Are your contracts versioned independently of any single service?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three yeses to the wrong questions, and the diagnosis writes itself: the services are coupled at the contract layer, and no amount of separate repos, pipelines, or deploy targets changes that.&lt;/p&gt;

&lt;p&gt;Decoupling was never a property of how many services you run. It’s a property of what they’re allowed to depend on.&lt;/p&gt;

&lt;p&gt;If your contracts aren’t independent, your services aren’t either.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build email infrastructure (rootmail, in beta) and write about backend systems every week. More here: &lt;a href="https://robertpaschal.substack.com" rel="noopener noreferrer"&gt;https://robertpaschal.substack.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>grpc</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>My site is done. It's not a portfolio — it's a menu.</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Wed, 21 Oct 2026 09:16:15 +0000</pubDate>
      <link>https://dev.to/robertpaschal/my-site-is-done-its-not-a-portfolio-its-a-menu-kmi</link>
      <guid>https://dev.to/robertpaschal/my-site-is-done-its-not-a-portfolio-its-a-menu-kmi</guid>
      <description>&lt;p&gt;I finished building heypascal.me this week, and I made a deliberate choice: it's not a portfolio. It's a menu.&lt;/p&gt;

&lt;p&gt;Eight ways to work with me, from a $100 retry-safety audit to a full MVP backend: backend APIs, AWS &amp;amp; deployment, document workflows, retry-safe systems, architecture reviews, MVP backends, technical writing, API docs.&lt;/p&gt;

&lt;p&gt;Everything is scoped and priced up front. I've been on the buying side of the "let's hop on a discovery call" maze enough times to know nobody enjoys it — so I killed it. You see the offering, the scope, the price. Then you decide.&lt;/p&gt;

&lt;p&gt;The retry-safety audit is the one I'm most proud of, honestly. Idempotency keys, duplicate-delivery handling, outbox patterns — the unglamorous stuff that decides whether your API survives its first traffic spike. Most teams don't know they need it until something double-charges a customer.&lt;/p&gt;

&lt;p&gt;If you've got something to build: &lt;a href="https://heypascal.me" rel="noopener noreferrer"&gt;https://heypascal.me&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>freelance</category>
      <category>product</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Every service sends emails — stop that</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:39:04 +0000</pubDate>
      <link>https://dev.to/robertpaschal/every-service-sends-emails-stop-that-169d</link>
      <guid>https://dev.to/robertpaschal/every-service-sends-emails-stop-that-169d</guid>
      <description>&lt;p&gt;A mistake I see in many microservice systems:&lt;/p&gt;

&lt;p&gt;Every service sends emails.&lt;/p&gt;

&lt;p&gt;Auth sends emails. Billing sends emails. User service sends emails.&lt;/p&gt;

&lt;p&gt;Soon you have 5 different email implementations, 5 retry strategies, 5 ways to get rate-limited.&lt;/p&gt;

&lt;p&gt;The fix: one notification service. Every service emits events — the notification service owns delivery, retries, rate limits, and user preferences.&lt;/p&gt;

&lt;p&gt;Notifications are deceptively complex. Once you add retries, rate limits, preferences, and multiple channels, it deserves its own service.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Your SES failover will never fire — and that's the point</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Mon, 19 Oct 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/robertpaschal/your-ses-failover-will-never-fire-and-thats-the-point-4a4o</link>
      <guid>https://dev.to/robertpaschal/your-ses-failover-will-never-fire-and-thats-the-point-4a4o</guid>
      <description>&lt;p&gt;A practitioner running 50k emails/day on SES told me his Postmark failover only triggers on SES errors. Sounds reasonable. Except a suppressed address never errors — SES accepts it, returns 200, and quietly drops it. The backup never fires.&lt;/p&gt;

&lt;p&gt;This is the trap: failovers are built around API errors, but deliverability dies in the silent middle — suppressions, throttled sends, greylisting. Your fallback logic watches the wrong signal.&lt;/p&gt;

&lt;p&gt;What actually works: treat the suppression list as a first-class delivery signal. Monitor bounces and complaints as events, not just error codes. If your failover can't see a suppressed address, it's not a failover — it's a decoration.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>email</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I gave an AI my job hunt. Here's the system that kept it honest.</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Sat, 17 Oct 2026 06:54:51 +0000</pubDate>
      <link>https://dev.to/robertpaschal/i-gave-an-ai-my-job-hunt-heres-the-system-that-kept-it-honest-46a8</link>
      <guid>https://dev.to/robertpaschal/i-gave-an-ai-my-job-hunt-heres-the-system-that-kept-it-honest-46a8</guid>
      <description>&lt;p&gt;Yesterday I applied to 12 engineering jobs. I didn't open a single application form.&lt;/p&gt;

&lt;p&gt;My AI chief of staff did it — research, verification, form-filling, submission, audit trail. I woke up to 12 submitted applications, 8 dead listings caught before I wasted a click, and 3 forms flagged as broken instead of faked.&lt;/p&gt;

&lt;p&gt;This isn't a "10x your job hunt with ChatGPT" post. It's about the verification layer that makes delegating to an AI actually work — because an AI that applies to jobs &lt;em&gt;wrong&lt;/em&gt; is worse than no AI at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The system has four stages, and the important thing is what happens between them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Research&lt;/strong&gt; — find candidate roles across job boards, company career pages, LinkedIn. Rank by hire probability, not salary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live verification&lt;/strong&gt; — open every listing and confirm it's real: no expired banner, an actual Apply control, remote terms that include Nigeria. Yesterday: 8 of 20 listings were dead on arrival. Stale aggregators are the norm, not the exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application&lt;/strong&gt; — fill forms with verified facts only. Resume, GitHub, portfolio attached. Every answer grounded in something checkable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit&lt;/strong&gt; — every submission logged in a tracker with the confirmation (or the honest lack of one), timestamped, with caveats noted.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honesty constraint
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised me. Asked about production LLM-API experience, the AI wrote: "No production LLM-API experience." Asked about shipped autonomous agents: "None." Asked about production Go: "No production Go."&lt;/p&gt;

&lt;p&gt;It disclosed what I &lt;em&gt;haven't&lt;/em&gt; done instead of inventing it. That's not a limitation of the system — it IS the system. A fabricated application that gets you an interview you can't survive is negative value. An honest application to the right role is the whole game.&lt;/p&gt;

&lt;p&gt;The rule I gave it: &lt;strong&gt;only those you record are true.&lt;/strong&gt; Every claim in every application had to trace back to a verified fact — my resume, my GitHub, my site. No invented years of experience, no inflated metrics, no phantom credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure modes (all real, all yesterday)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broken submit handlers.&lt;/strong&gt; Three application forms (all on the same company's careers site) filled completely, passed validation, then silently refused to submit — the Send button dispatched and nothing happened. The system stopped, flagged them for manual action, and logged them as Drafting. It did not fake a confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead listings.&lt;/strong&gt; 8 of 20 from aggregators were expired, closed, or region-locked. Caught at verification, never shown to me as options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The honesty tax.&lt;/strong&gt; Disclosing "no production LLM experience" on an AI Engineer application probably tanks that application. Good. I'd rather lose honestly than win fraudulently and get exposed in a technical screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;The verification stage needs to run &lt;em&gt;before&lt;/em&gt; research finishes ranking — I burned research effort on roles that died at verification. And the broken-form problem needs a human-in-the-loop escape hatch earlier: three fully-filled forms sitting in Drafting is a queue, not a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable takeaway
&lt;/h2&gt;

&lt;p&gt;The bottleneck was never my ability. It was my bandwidth. Twenty applications in a day is a data-entry problem, and data entry is exactly what machines are for — as long as the verification layer is ruthless.&lt;/p&gt;

&lt;p&gt;The engineers who thrive with AI won't be the ones who prompt hardest. They'll be the ones who build verification into the loop.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Odinaka Nnamani — backend engineer (TypeScript, Node.js, PostgreSQL, AWS) and founder of &lt;a href="https://rootmail.io" rel="noopener noreferrer"&gt;Rootmail&lt;/a&gt;, email infrastructure that tells you what happened to every email. Open to remote backend/infrastructure roles: &lt;a href="https://heypascal.me" rel="noopener noreferrer"&gt;heypascal.me&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I launched heypascal.me — here's what I built, what broke, and what I'd do differently</title>
      <dc:creator>Odinaka Robert Nnamani</dc:creator>
      <pubDate>Fri, 16 Oct 2026 22:02:09 +0000</pubDate>
      <link>https://dev.to/robertpaschal/i-launched-heypascalme-heres-what-i-built-what-broke-and-what-id-do-differently-4hcl</link>
      <guid>https://dev.to/robertpaschal/i-launched-heypascalme-heres-what-i-built-what-broke-and-what-id-do-differently-4hcl</guid>
      <description>&lt;p&gt;heypascal.me is live. Let me tell you the real story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I built:&lt;/strong&gt; A personal site — portfolio, blog, and services page in one. Static HTML/CSS/JS on S3 + CloudFront, GitHub Actions deploys, Lucide icons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; I restarted three times. v1 was a portfolio template mindset — felt fake. v2 was blog-only — felt incomplete. The breakthrough was realizing the site had to be all three things I am: engineer, founder, writer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical opinions baked in:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content over chrome: no hero carousel, no stock photos&lt;/li&gt;
&lt;li&gt;"Contact for quote" is a dark pattern: 8 services, all with starting prices ($80–$800)&lt;/li&gt;
&lt;li&gt;Performance as a feature: fast, low-data friendly&lt;/li&gt;
&lt;li&gt;Zero frameworks: sometimes hand-rolled is the right call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I'd do differently:&lt;/strong&gt; Start with the positioning question, not the tech. I burned weeks on pixel decisions for a site whose purpose I hadn't nailed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next:&lt;/strong&gt; The "Work with me" section is live with real offerings. If you're a founder who needs backend work, AWS setup, or technical writing — that's what it's for.&lt;/p&gt;

&lt;p&gt;heypascal.me — would love your teardown.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>career</category>
      <category>personal</category>
    </item>
  </channel>
</rss>
