Tobby Umoh · Lagos, Nigeria · Frame 001

I makecomplicated thingsfeel inevitable.

Backend engineer, product builder, and persistent student of what happens when software meets money, people, failure, and the real world.

Enter the story
A portfolio in four acts
Best experienced by scrolling
Act one · The craft

I build the parts people should not have to think about.

The transfer that must not duplicate. The background job that must finish. The webhook that arrives twice. The business operation that fails behind a perfectly respectable HTTP 200. My work is making those systems understandable, recoverable, and boring in production.

01Reliability is designed, not added later.
02Architecture is a product decision in technical clothing.
03Observability earns its keep only when it leads to action.
Act two · Selected work

Three frames from a much larger reel.

01 / ProductPrivate beta

Foveus

Execution intelligence for .NET systems. Built to explain what actually happened across requests, dependencies, jobs, and business outcomes when “the logs look fine” is not enough.

.NETExecutionProduct
02 / Production practicePayments & identity

Systems that move money.

Transfer flows, limits, identity decisions, delayed validation, retries, failure recovery, and reconciliation. The useful story is not a company logo. It is the discipline required when mistakes have consequences.

ReliabilityIdempotencyOperations
03 / ArchitectureZero to one

Cross-border, without the chaos.

A modular backend foundation for cross-border transfers: quotes, KYC, payouts, webhooks, ledgering, reconciliation, and operations. Designed to move quickly now without sabotaging scale later.

Modular monolithLedgerScale
View the complete work archive
Intermission · Beyond the laptop

Not everything meaningful needs to scale.

I am learning to leave room for things that do not need a roadmap: books read slowly, stage plays across Lagos, a home that feels like mine, and a family of cats who treat every quiet moment as shared property.

Final frame, for now

The credits are still rolling.

This website is not a finished portrait. It is a record of serious systems, unfinished ideas, clearer thinking, and a life that is becoming larger than the work.

Start a conversation
The complete reel · 2019—now

Work.

Products, production systems, architecture, experiments, and useful side quests. The homepage shows the highlights. This is where the range lives.

Open any entry for the full context
01

Foveus

Product · Private beta

Execution intelligence for .NET systems.

+

I started Foveus because production failures are often hidden behind technically successful requests, fragmented logs, and missing business context. It reconstructs execution timelines across inbound requests, outbound dependencies, jobs, and business outcomes, then turns those executions into issues a team can actually act on.

Open related work ↗
RoleFounder & backend engineer
StagePrivate beta
Stack.NET, PostgreSQL, Timescale, Redis
FocusExecution timelines, issues, SDK design
02

Systems that move money

Production practice · In production

Transfer reliability, limits, delayed validation, and recovery.

+

This is a body of work rather than a single public product: money movement flows, KYC-driven transaction limits, delayed validation, retries, background processing, idempotency, and operational recovery. The common thread is designing for the moments when networks, providers, replicas, and humans behave unpredictably.

RoleBackend engineer
StageOngoing
Stack.NET, SQL Server, Redis, Hangfire
FocusReliability, operations, financial correctness
03

Cross-border transfer foundation

Architecture · In development

A scalable modular backend for cross-border remittance.

+

The goal was not to draw an impressive distributed system on day one. It was to create a modular monolith with explicit boundaries for transfers, quotes, KYC, ledgering, payouts, webhooks, reconciliation, refunds, notifications, and audit, plus the operational patterns needed to scale safely.

RoleBackend & architecture lead
StageIn development
Stack.NET 8, PostgreSQL, Docker
FocusModular monolith, outbox, idempotency
04

Sendbyte .NET SDK

Open source · Open source

A typed .NET client and webhook verifier for an email platform.

+

A focused open-source contribution: a clean .NET SDK structure, options-based configuration, IHttpClientFactory support, email and domain clients, webhook verification, tests, samples, and packaging decisions that make the library feel native to the ecosystem. Useful work, but intentionally not presented as a flagship product.

Open related work ↗
RoleOpen-source contributor
StagePrerelease
Stack.NET Standard 2.0, .NET 8
FocusSDK ergonomics, testing, packaging
05

Failure Lab

Engineering playground · Active

A controlled environment for making distributed failures visible.

+

A test harness for reproducing timeouts, nested HTTP failures, business failures behind successful responses, and multi-service execution chains. It exists to keep product claims honest and to turn vague observability ideas into concrete behaviours that can be tested repeatedly.

RoleDesigner & engineer
StageActive
Stack.NET, Docker, HTTP test services
FocusFailure modelling, demos, regression tests
06

Outcome semantics

Research & product · Exploring

A portable way to describe whether work truly succeeded.

+

Transport success, technical completion, and business success are not the same thing. This work explores a small semantic model for recording business outcomes, expected continuations, and asynchronous lineage in a way that starts naturally in .NET but can travel across services and languages later.

RoleProduct research
StageExploring
Stack.NET, OpenTelemetry concepts
FocusBusiness outcomes, async lineage, portability
Notes from the cutting room

Blog.

Essays about reliable systems, product decisions, and the rest of life. Technical depth without pretending every lesson is universal.

Six complete sample essays
← Back to the journal

A 200 OK is not proof that the operation succeeded.

The transport layer can tell the truth while the business operation fails. A practical way to reason about success in distributed applications.

We have trained ourselves to read a successful HTTP status as the end of a story. The request returned 200. The dashboard is green. The endpoint is healthy. Yet the customer still has no value, the ledger is unchanged, or the downstream operation quietly moved into a failed state.

Transport success is only one layer

An HTTP status answers a narrow question: did this server accept and respond to this request in a way the protocol considers successful? That matters, but it is not the same as asking whether the business operation achieved its intended outcome.

Consider a transfer API that accepts a request, records it as pending, dispatches work to a provider, and returns immediately. A 200 OK or 202 Accepted may be perfectly honest. The API did its part. The provider can still reject the transfer ten seconds later. The customer experiences failure even though every synchronous span looks healthy.

Technical completion and business success are related, but they are not synonyms.

Model the outcome explicitly

The mistake is not asynchronous processing. The mistake is leaving the final meaning implicit. A useful execution model should be able to answer four different questions:

  • Did the request complete?
  • Did the technical workflow complete?
  • Did the intended business outcome occur?
  • If not, where did reality diverge from expectation?

That usually means recording an outcome close to the domain, not trying to infer everything later from status codes and log text.

execution.SetOutcome(
    status: OutcomeStatus.Failed,
    code: "PROVIDER_REJECTED",
    reason: "Beneficiary account could not be credited");

Follow the operation, not only the request

In distributed systems, the unit of interest is often larger than one HTTP request. It may include an inbound call, two outbound dependencies, a database write, a queued continuation, and a webhook that arrives later. Treating each piece as an isolated success creates a collection of locally correct statements that still fail to explain the customer’s experience.

The operational question should become: what was supposed to happen next, and did it happen? That leads naturally to execution timelines, parent-child relationships, expected continuations, and explicit business outcomes.

What teams should alert on

Alerting on every 500 response creates noise. Alerting only on infrastructure health misses business failures. Better signals combine the two. A spike in provider timeouts matters. So does a rise in executions where the transport succeeded but the outcome became failed, abandoned, or unresolved.

The goal is not to produce more telemetry. It is to shorten the distance between a customer-visible failure and a precise explanation.

The useful definition of success

Success is not “the handler did not throw.” Success is the intended state transition happening, with enough evidence to trust that conclusion. Sometimes that conclusion is available immediately. Sometimes it arrives through a job, provider callback, or reconciliation process. The architecture should make that delay explicit rather than pretending the first response contains the whole truth.

A 200 can be correct. It just should not be mistaken for the ending.

← Back to the journal

A life that is not another project plan.

Books, stage plays, kittens, and the uncomfortable art of doing things that produce no measurable output.

Work expands easily when it is the place where competence feels most reliable. There is always another system to improve, another course to finish, another product decision to revisit. A quiet weekend can become a missed opportunity before it has even started.

Hobbies feel inefficient at first

When you are used to measurable progress, reading a novel slowly or travelling across Lagos to watch a stage play can feel indulgent. There is no repository at the end. No certification. No obvious compounding advantage.

That may be exactly why it matters.

A good life cannot be reduced to the parts that improve your résumé.

Start before you become the kind of person who starts

It is tempting to wait until you have built a reading habit before joining or starting a book club. But identity often follows participation. Read one book. Invite a few people. Let the first meeting be imperfect. The habit can grow around the life instead of being a prerequisite for it.

Let frequency match reality

A stage play once a month is still a hobby. Cost and distance do not invalidate it. A hobby does not need to occupy every Saturday to count. The goal is not to win at leisure. It is to create recurring experiences that are not delivered through a screen.

Document without turning it into a brand

Posting kittens, books, a theatre ticket, or a corner of a new apartment can be a way of paying attention. It does not need a content strategy. Some photographs can stay private. Some posts can be ordinary. Not every interest needs a niche.

Leave room for surprise

The most interesting version of a life is difficult to plan from a desk. It needs invitations, inconvenient trips, books chosen for no professional reason, people you did not meet through work, and afternoons that do not justify themselves.

The project plan can organise the move, the budget, and the calendar. It cannot manufacture aliveness. That part requires showing up.

← Back to the journal

Building a product before you have permission.

The useful kind of conviction is not certainty. It is enough evidence to take the next honest step.

There is a particular loneliness in building a product before the market has agreed that it should exist. You can see the problem clearly. You can also see ten reasons the product may fail, five competitors you have not fully understood, and a version of the future where nobody cares.

Conviction is not the absence of doubt

Useful conviction is the willingness to keep testing an idea without turning every encouraging signal into proof. A frustrated engineering conversation is evidence of pain. It is not evidence that someone will install, trust, or pay for your product.

Build enough to make the next conversation real.

Private beta is a learning instrument

The first version does not need every future capability. It needs a narrow path through installation, useful evidence, and one or two moments where the user says, “I would not have understood this as quickly without it.”

For a developer tool, that means the onboarding experience, the quality of captured data, the reliability of the SDK, and the clarity of the first useful screen matter more than a long roadmap.

Choose users who can hurt the idea

Friendly praise feels good and teaches little. The right early users have real systems, real constraints, and enough trust to tell you where the product is wrong. Their objections are not obstacles to defend against. They are pressure tests for the product’s assumptions.

Protect the ambition by narrowing the promise

A broad vision can survive a narrow first release. The opposite is harder. Saying less allows the product to deliver something precise. The roadmap can still include execution graphs, asynchronous lineage, agents, and cross-language semantics. The beta only needs to solve the first painful slice exceptionally well.

Permission arrives after evidence

Nobody grants a founder permission to take an idea seriously. The work is to earn stronger reasons: a successful installation, a repeated use case, a user who asks when the next version is coming, a team willing to keep the SDK in a real service.

You do not need certainty. You need the next honest piece of evidence.

← Back to the journal

A modular monolith is not an apology.

Choosing cohesion over ceremony while a product is still discovering its real boundaries.

Some architecture diagrams look like ambition. Others look like fear dressed as ambition. A young product with six services, three queues, two databases, and a service mesh may feel serious, but seriousness is not measured by the number of deployment units.

The real question is where change belongs

A modular monolith is one deployable application with deliberate internal boundaries. Transfers do not reach directly into KYC tables. Reconciliation does not mutate payout state by convenience. Modules communicate through explicit contracts, even though they share a process and may initially share a database.

This gives a team something more valuable than fashionable topology: a clear place for each decision.

A boundary is useful before it becomes a network call.

Microservices charge rent immediately

Independent services can be the right answer. They also introduce distributed transactions, deployment coordination, versioned contracts, cross-service observability, network failure, local development complexity, and more operational surface area. Those costs arrive on day one. The benefits often arrive only after the organisation, traffic, and domains have grown enough to need them.

For a small team building a financial product, the early constraint is rarely CPU. It is the ability to understand the system, make changes safely, and keep product behaviour coherent while requirements are still moving.

Modularity must be real

Calling a folder structure modular does not make it so. Useful modules own their data, expose narrow contracts, and avoid circular dependency. Cross-cutting concerns such as audit, idempotency, and outbox delivery should support the modules rather than dissolving their boundaries.

Transfers
  ├── Application
  ├── Domain
  ├── Infrastructure
  └── Contracts

Ledger
  ├── Application
  ├── Domain
  ├── Infrastructure
  └── Contracts

The code should make the desired architecture easier than the accidental one.

Scale the pressure point, not the diagram

If quote generation becomes computationally heavy, extract or scale that capability. If webhooks need independent throughput, move their workers. If a provider integration changes at a different pace, isolate it. A modular monolith preserves these options because the boundaries already exist.

The extraction should follow evidence: load, team ownership, failure isolation, release cadence, or regulatory separation. “We may have many users” is not evidence that every module needs a network boundary today.

It is a strategy, not a compromise

The best early architecture is not the one that proves you understand every distributed systems pattern. It is the one that preserves speed, correctness, and optionality. A modular monolith can handle significant scale, especially when supported by sensible database design, caching, background processing, and horizontal application scaling.

It is not an apology for avoiding microservices. It is a refusal to pay for complexity before complexity has earned its place.

← Back to the journal

The quiet work behind moving money.

The visible feature is “send.” The real product is the collection of safeguards around everything that can happen next.

A transfer button compresses a remarkable amount of uncertainty into one gesture. The customer sees an amount, a beneficiary, and a confirmation screen. The backend sees identity, limits, balance, fees, provider availability, duplicate submission, delayed settlement, callbacks, reconciliation, and the possibility that two systems disagree about reality.

Money movement is state management with consequences

The core engineering challenge is not sending one request to a provider. It is preserving a trustworthy state machine while every participant can fail independently.

An instruction may be created, validated, accepted, dispatched, acknowledged, completed, reversed, or left in an unknown state. Those states need precise transition rules. “Pending” cannot become a dumping ground for anything the system does not understand.

Financial correctness is the product, not an implementation detail.

Unknown is a real state

When a provider times out, the operation may have failed or succeeded. Treating that as a simple failure invites duplication. Treating it as success misleads the customer. The honest state is unknown until a status query, callback, or reconciliation process resolves it.

Idempotency must survive restarts

An in-memory lock is not enough. The system needs a durable mapping between a client’s operation key and the resulting instruction. Repeated requests should return the same logical result, even after a deployment or process crash.

clientReference + customerId
        ↓
unique operation record
        ↓
same response or current state

Operations are part of the architecture

Some states cannot be resolved automatically. Back-office tools, audit trails, reconciliation views, maker-checker controls, and clear ownership are not secondary admin features. They are how the product remains correct when automation reaches its limit.

The customer needs a truthful story

A good API and user interface distinguish accepted from completed, retrying from unresolved, and failed from reversed. Internal nuance should not become customer confusion, but hiding uncertainty creates worse confusion later.

The quiet work behind moving money is making sure every possible ending has a name, an owner, and a path forward.

← Back to the journal

Retries are not a reliability strategy.

Repeating work can recover from transient failure. It can also duplicate money, amplify outages, and hide broken ownership.

A retry is seductive because it turns failure into a loop. Something failed; try it again. Sometimes that is exactly right. Sometimes the second attempt creates a second transfer, a second email, or a larger outage than the one you started with.

First decide whether repetition is safe

Before retrying an operation, ask whether the action is idempotent. Reading a resource is usually safe. Creating a financial instruction may not be. The system needs a stable idempotency key and a durable record of the first attempt before repetition becomes trustworthy.

The question is not “can we retry?” It is “what does the second attempt mean?”

Classify failure before reacting

A timeout does not prove the provider did nothing. It proves you did not receive a response in time. Retrying immediately may duplicate an operation that already succeeded. Validation errors, authentication failures, and deterministic business rejection should not be retried at all. Rate limits and transient network errors may deserve backoff.

  • Transient: retry with bounded exponential backoff.
  • Unknown outcome: query status or reconcile before repeating.
  • Permanent: fail clearly and stop.
  • Operational: route to manual or automated recovery.

Queues do not remove responsibility

Background jobs make retries convenient, which can make them invisible. A framework may retry twenty times while the product remains stuck and nobody knows who owns the final state. Every retry policy needs a terminal path: dead-lettering, escalation, compensation, reconciliation, or a state the customer can understand.

retry:
  maxAttempts: 5
  backoff: exponential
  onUnknownOutcome: reconcile
  onExhausted: markForOperationsReview

Retry budgets protect the system

During a provider outage, thousands of independent jobs can become a self-inflicted denial of service. Jitter, concurrency limits, circuit breakers, and retry budgets prevent recovery traffic from overwhelming the dependency and your own workers.

Reliability is the whole recovery design

Retries are one tool inside a larger strategy that includes idempotency, durable state, reconciliation, observability, ownership, and explicit customer outcomes. Used carefully, they turn transient failure into a small delay. Used blindly, they turn uncertainty into duplication.

The loop is not the strategy. Knowing what the next attempt means is.

Single-file cinematic preview · Home / Work / Blog / Articles