Senior Software Engineer · Technical Leader
Distributed systems · Payments · Industrial IoT · Agent tooling for engineering teams
I spent 8 years and 8 months assembling product on a factory line. Today I build the platform that monitors that same shop floor: machine telemetry over MQTT, real-time OEE, and a rules engine the operations team reconfigures without asking anyone for a deploy.
I know which questions the operator asks because I used to be the operator. That is the part of my background that explains most about how I work — I never had to learn the business, I already was the business.
I moved into tech through the help desk in November 2021 and pushed my first commit in August 2022. Four years later I answer technically for the cross-cutting systems of a group that runs manufacturing, retail, banking, consortium and insurance.
Engineering with agents · Scope · What I build · Open source · How I decide · Stack · Trajectory
This is the most relevant thing I do today, and it is not writing code with an assistant. I built the engineering toolchain my area uses to work with agents — and it stopped being mine. It is now an internal product any engineer installs with one command.
The house tools became commands the agent operates. The house standards became instructions it loads on its own. The knowledge of the estate became a queryable base that updates without me. And the daily friction became the input of a cycle that produces new tooling.
| Distributed skills | 11 — one per tool or operational domain |
| Automatic hooks | 5, paired for the two operating systems the team runs |
| Command-line tools | 8, ~2,540 lines — service desk, database, logs, identity, versioning, standards |
| Knowledge base | 681 application records, plus 20 code-standard rules |
| Tests | 3,798 lines — the toolchain has more test than CLI |
flowchart TB
CLI["<b>Tools as CLIs</b><br/>8 commands<br/><i>GitLab · database · logs<br/>service desk · identity</i>"]
STD["<b>Standards the machine consumes</b><br/>11 skills · 5 hooks · 20 rules<br/><i>one neutral source,<br/>thin per-vendor adapters</i>"]
MAP["<b>The estate map</b><br/>681 application records<br/><i>scanned, self-updating</i>"]
CLI --> KIT
STD --> KIT
MAP --> KIT
KIT["<b>One command installs and updates all of it</b><br/><i>idempotent · rewrites what changed version<br/>· never touches the user's own content</i>"]
KIT --> CYC
CYC(["<b>The cycle that feeds itself</b><br/>friction → problem log → detected pattern → new skill<br/><i>the only step I do by hand is approving</i>"])
CYC -.->|"generates the next tool,<br/>the next skill, the next record"| KIT
classDef piece fill:#512BD4,stroke:#512BD4,color:#fff
classDef hub fill:transparent,stroke:#888,color:#888
class CLI,STD,MAP piece
class KIT,CYC hub
Knowledge lives once; the adapter is disposable. What a tool does, what trap it has, what the house standard is — that lives in a single layer. The format a specific AI product consumes lives in a thin one beside it. Swapping vendors tomorrow means writing a sibling adapter, not rewriting years of operational knowledge. Agent tooling is the fastest-moving market there is; binding the company's knowledge to one vendor's file format is buying a migration with a date on it.
Synchronization, not installation. The command is not a first-run command — it runs as many times as needed. Toolchain parts that changed version get rewritten; the user's own content is never touched. That separation is what makes one command serve both install and update, and what made it safe to distribute to the team. It stopped being called "install" precisely because the wrong name invited the wrong use.
Write access graded by blast radius. The whole toolchain belongs to the team and anyone may propose a change. But the pieces whose failure travels far — the commit hooks, the context common to every session, the code standards, the commands that write to sensitive systems, the synchronizer itself — require review from whoever leads. Not hierarchy by status: a bug in a lookup skill inconveniences whoever ran it; a bug in the commit hook reaches every machine on the team on its next run.
Standards written for humans have a known problem: nobody reads them at the moment it matters. Standards written for an agent have a quieter one — they keep looking correct after they become wrong. A document teaching a method whose signature changed breaks nothing. It just teaches the wrong thing, to everyone, indefinitely.
So there is a project that consumes every API the standards cite, and compiling is the assertion. If a standard describes a method that no longer exists, that is not a doc quietly going stale — it is a red build. Paired with automatic dependency updates and a scheduled run, because the drift never originates from a commit in the standards repo: it originates from a package changing out there.
The cycle, end to end — one real case
The best example of the cycle closing was not one problem. It was three that did not look like the same problem.
First symptom. A package install failed saying it could not resolve the host. Right after,
every curl started answering HTTP 200 — with the HTML of a login page in the body. I went
through "the package manager broke" and "DNS issue" before landing on the cause: the network edge
appliance session had expired and the captive portal was intercepting everything, answering 200.
About eight attempts. The wrong hypothesis was reasonable — the first error genuinely was name
resolution.
Second symptom, days later. A mobile build could not resolve a dependency, while curl against
the same host worked. That makes no sense until you find out the same appliance does selective
TLS inspection — intercepting one vendor's artifact repository and not the language's public one —
and that each tool picks its own truststore: curl uses the OS one, where the corporate CA is
installed; the build uses the JDK's, where it is not. Three attempts.
Third symptom. The app froze on "no connection" in the emulator and looked like an app bug. I killed two framework hypotheses before finding it: the OS runs an HTTPS network-validation probe, the probe failed against the corporate CA, and the system flagged the network as partial connectivity. Without the validated-network flag, the app's library was only repeating what the system said. About ten attempts.
The crossing. Three incidents, three symptoms with no apparent kinship — name resolution, a build, emulator connectivity. None of them alone revealed the pattern. Reading the accumulated log did: it is one appliance with three faces. An entire class of problem, not three accidents.
What it became. A network skill the agent loads on its own when a symptom from that family
shows up. It describes the three faces, explains why curl works and the build does not, and
carries the procedure for each case — including where the exported CA lives, so it goes into the
right truststore.
Cost before and after. Across the three incidents: twenty-plus attempts and a day lost to a diagnosis that went through package manager, DNS, framework and app bug — none of them the cause. Today the symptom is recognized in the first message of the conversation, with the procedure attached.
And one case where the cycle deliberately produced nothing
This one matters more, because it shows the judgment step is real.
Common commands kept failing with "command not found", including basic system utilities. It looked
like a missing package. The real cause was the shell PATH collapsing now and then, losing even
the default system directories. It happened twelve times, across five projects, over a month.
Twelve recurrences is exactly the pattern that makes the system propose a skill. I reviewed it and
decided it does not become one. A skill delivers knowledge to an agent; the problem here is not
missing knowledge, it is a broken environment. A skill saying "when PATH disappears, re-export
PATH" would automate living with a defect instead of fixing it — and worse, would make the defect
look solved.
The entry stays in the log, marked reviewed, which is what it should be: a known diagnosis waiting for a fix at the source. Not every recurring friction is a tooling opportunity. Some are just debt waiting for an owner, and telling the two apart is the one thing in the cycle that cannot be automated.
Because the bottleneck moved. When implementing gets cheap, the value migrates to whoever can define the problem, verify the result, and organize the context the work happens in.
And there is a consequence I did not predict: the agent fails exactly where the documentation fails. Where the convention is implicit, where the rule lives in someone's head, where "that's just how we do it here" was never written down — that is where the output comes out wrong. Working this way forced me to make explicit what I should already have made explicit for people. The quality gain for the team came along for free, and it was not the stated goal.
| 319 | applications in my area |
| 128 | under my direct technical responsibility |
| 118 | repositories containing my code |
| 32 | systems where I am the principal author |
Sole author of 5 of those 32; among the top three contributors in 65 more repositories.
| Measure | Value |
|---|---|
| My own code alive at HEAD | 112,461 lines, measured with git blame |
| Long ownership | 20 systems maintained for three years or more |
| Reach | 19 · 15 · 12 systems consuming the payments hub, document management and the email service |
| Shared libraries | contributor to 27, principal author of 8 |
| Open source | 5 public packages |
| Automated tests | two thirds of the repositories I touch have a suite |
Measured across the estate and my own repositories, as of August 2026.
A paid order is not a released order. Between the two sits a sequence of checks — acquirer confirmation, antifraud, a decision engine, sometimes a human. Which steps apply depends on the combination of company, business, operation type, channel, country and branch, plus the payment method used. That space grows every time the group opens a new business.
Multi-acquirer orchestration where a core decides the path and each payment method is a satellite service that only knows its own part: card, Pix, boleto, voucher, link, digital wallet. A transaction state machine with transitions written as an explicit ordered chain. A historical risk dossier — windowed counting, recurrence, fuzzy address comparison, blocklist crossing. Refunds with proportional apportionment and idempotency by source identifier. Revenue from e-commerce, the app and 200+ stores flows through it, plus the group's own financial products.
The decision: the verification policy of each channel is data, not code. Changing a whole business's antifraud policy stopped requiring a release. The cost is real — configuration became critical system state and needs care of its own. I took it because a wrong configuration is reversible in minutes and a wrong deploy is not.
OEE platform reading machine telemetry off the shop floor. Ingestion over MQTT — a device protocol, not an application one: small messages, persistent connection, tolerance for bad networks. Time series in TimescaleDB with retention windows and continuous aggregates. Real-time panel over SignalR. A PWA with local state that keeps operating with the network down and reconciles afterward. Back end and front end, same author.
The decision: offline was a requirement decided before the client was written, not a second-phase feature. Shop-floor connectivity drops — interference, uneven coverage between sheds, equipment competing for spectrum. A system that needs a connection to record a reading gets worked around with paper, and then the data feeding the indicator is fiction.
Sales commission for five channels that pay differently: physical retail, e-commerce, wholesale, B2B, and an operation in another country. From a distance the five rules are the same thing. Up close they diverge in everything that matters — calculation base, when it is assessed, what retroacts, how apportionment treats a team, what happens when a sale is cancelled after closing.
The decision: one implementation per channel, not a parameterized engine. Parameterizing a formula produces a programming language badly disguised as configuration. It aged better than I expected, for a reason I had not anticipated: the largest repository in the area went through 29 different authors, and independent, readable code survived that. Each person changed the channel they needed without understanding the other four.
Integrating is not moving data, it is translating models — and deciding what to do when the translation is not exact. SAP as ERP, with internal SDKs standardizing authentication, OData and sales orders. Apple GSX, DEP and Trade In behind technical assistance and buyback. Zendesk for support, Emarsys and Oracle Responsys for marketing.
The decision: one translator per external platform, not a generic bus. A single bus looks like savings and becomes a queue: every team waits on one team, and one platform's incompatibility contaminates the shared model.
Migrating a corporate estate from IIS on Windows servers to containers on Kubernetes. Worth being precise about what was already solved: the estate already had automated delivery — GitLab CI building, testing and publishing, separate environments, working rollback. What was legacy was not the delivery process, it was where the application runs.
What actually changes: reproducible environments, CronJobs instead of Windows Schedule, and monitoring the team can read without filing a ticket. 33 applications on the cluster, 194 still on Windows servers — 12 of the migrated ones are mine. I am a competent consumer of that platform, not its owner: the cluster, the base charts and the GitOps pipeline are the DevOps team's work.
Five .NET libraries extracted from systems already running in production, then generalized. This is the part of my work anyone can read today, with no NDA involved.
| Package | What it solves | Version | Downloads |
|---|---|---|---|
| PMQ.Notifications | Broken business rules as accumulated data instead of exceptions | ||
| PMQ.Mediator | CQRS mediator with pipeline behaviors, streaming and FluentValidation | ||
| PMQ.ErrorHandling | Centralized ASP.NET Core error handling with RFC 9457 responses | ||
| PMQ.Identity | Provider-agnostic authentication — external OIDC or self-issued JWT | ||
| PMQ.Domain | DDD building blocks — entities, aggregate roots, value objects, domain events |
They snap together into one request lifecycle — the fit is the thesis:
flowchart TB
REQ(["HTTP request"]) --> ID
subgraph EDGE [" Edge "]
ID["PMQ.Identity<br/><i>who is calling</i><br/>OIDC or self-issued JWT"]
end
ID --> MED
subgraph APP [" Application "]
MED["PMQ.Mediator<br/><i>dispatches the use case</i>"]
VAL{"validation<br/>pipeline"}
HND["handler<br/><i>one use case</i>"]
MED --> VAL
VAL -->|"valid"| HND
end
subgraph DOMAIN [" Domain "]
DOM["PMQ.Domain<br/><i>entity · aggregate<br/>value object · event</i>"]
NOT["PMQ.Notifications<br/><i>the rule complains here,<br/>accumulating</i>"]
DOM -->|"rule broken"| NOT
end
HND --> DOM
VAL -.->|"reads"| NOT
VAL -->|"invalid:<br/>never executes"| ERR
subgraph OUT [" Output "]
ERR["PMQ.ErrorHandling<br/><i>standard response</i><br/>RFC 9457"]
end
HND --> OK(["200 · result"])
ERR --> PROB(["4xx · problem details<br/>with every error"])
classDef pkg fill:#512BD4,stroke:#512BD4,color:#fff
classDef plain fill:transparent,stroke:#888,color:#888
class ID,MED,DOM,NOT,ERR pkg
class VAL,HND plain
The notification the domain accumulates is the same one the mediator pipeline reads before executing, and the same one the error handler turns into an HTTP response. Domain events publish after the commit — a handler must never observe a fact the transaction rolled back.
A violated business rule is an expected outcome, not exceptional control flow. Entities accumulate their failures instead of throwing — which costs less, and reports every problem at once instead of only the first:
var order = Order.Create(request.Items); // never throws
if (order.IsInvalid)
{
notificationContext.AddFrom(order, NotificationType.BusinessRule); // → HTTP 422
return Guid.Empty;
}Exceptions stay where they belong: programming errors.
What the client gets back
One request, every broken rule — instead of discovering them one deploy at a time:
{
"title": "A business rule validation failed.",
"status": 422,
"errors": [
{ "field": "Items", "message": "Item must be at most 200 characters." },
{ "field": "Items", "message": "Provide at least one item." }
],
"traceId": "00-079d247677d55f7d8427fd421daec5f0-a773dd27f85d9bdb-01"
}Extracted after the third case, never on the expectation of reuse — an abstraction built in anticipation is almost always wrong.
Five positions I hold, each with the price I agreed to pay for it.
Rules as data when the rule belongs to the business. Whenever a rule changes at the speed of the business, it leaves the code and becomes configuration — the channel risk policy, the factory downtime engine. The cost: configuration becomes critical system state and needs care of its own. The limit: in commissioning I decided the opposite, because there what varies is the formula, not which steps of a known sequence apply. Knowing where the rule stops applying is part of the rule.
Extract after the third case. Only the third use shows what is essential and what was an accident of the first. All 8 shared libraries I own and all 5 public packages came out of code that was already running in more than one place. The cost: duplication lives longer than it looks comfortable to leave it.
Derived state, never assigned. When an aggregate's state depends on its parts, that state is
calculated, not written. Writing status = completed creates the chance for the status to lie.
The cost: a recalculation function. The return: an entire category of support bug disappears.
Idempotency on everything coming from outside. External integrations resend, callbacks arrive out of order, robots reprocess queues. Any operation reachable by a third party must answer the same thing when called twice.
Say what I don't know. A profile with no declared limits forces the reader to find the limits themselves, and the first discovery contaminates everything that came before it. I would rather the limit be written down before anyone has to ask.
The level is the part that matters here. Plenty of people list twenty technologies without distinguishing what ran in production from what was a tutorial, so the distinction is explicit. Presence means a declared dependency in a project file, not a mention in code.
Applied in production
| Level of presence | |
|---|---|
| .NET / C# | every repository of mine · 224 of the estate's 319 apps · versions 2.2 through 10 |
| Oracle · EF Core · Dapper | 57% · 54% · 21% of my repositories |
| xUnit · FluentValidation | 65% · 54% of my repositories |
| React and TypeScript | 37 apps in the estate · 3 products of mine with back and front |
| Angular | 14 apps in the estate — maintenance, not new architecture |
| MongoDB · PostgreSQL · TimescaleDB | 6% · 4% · one system, telemetry time series |
| Kubernetes · ArgoCD · Helm · Docker | 11% of my repos · 33 estate apps on the cluster |
| GitLab CI | delivery for the whole estate, legacy and new |
| MQTT · SignalR | one system each — machine telemetry, real-time panel |
| Keycloak · OIDC · JWT | estate authentication |
| Polly | 6% of repos — retry and circuit policies |
Declared limits — what I know without production to prove it
| Technology | Status |
|---|---|
| Broker messaging (RabbitMQ, Kafka) | knowledge, no production. Does not appear in a single repository of mine. It is the gap I would close first |
| gRPC | knowledge, no production system |
| Event sourcing | studied, never applied |
Two more, from the projects themselves: I have no mileage with TimescaleDB at scale or advanced tuning, and I have never operated a Kubernetes cluster during a real incident — when something breaks at the infrastructure level, I am not the one who fixes it.
Two games, both private repositories, both built for family rather than for a store. They are here for what they prove.
Onde está o Gato — 2D Android game, Godot 4 and C#. The game rules do not know the engine: the domain is a pure C# library with no line of Godot in it, and the engine project is a thin layer that reads it and draws it. Most of the game is testable without a GPU, and the whole suite runs headless in seconds — which matters, because I develop on a dual-core Pentium G4560 where opening an Android emulator to check a progression rule is not viable in practice, and what is not viable in practice nobody checks.
Reino do Ritmo — musical game for Roblox, in Luau. Same decision, different stack: the core is
pure Luau with no game, workspace, Instance or task, holding economy, rarity rolls, offline
income and streaks. Keeping it pure took three layers — a linter configured against the standard
library instead of Roblox's, a CI guard against os.time, tick and math.random, and the suite
running outside Roblox, which is the one that actually proves it.
The same architectural decision surviving C#/Godot and Luau/Roblox is the best evidence I have that the principle does not depend on the stack. What did not translate is documented with the same honesty — identity, repositories and domain events gain nothing in a single-aggregate save file, and pretending otherwise is how pattern translation turns into cargo cult.
| Period | Role | Duration |
|---|---|---|
| Apr 2013 – Nov 2021 | Production Assembler | 8 y 8 m |
| Nov 2021 – Jul 2022 | IT Help Desk Assistant | 9 m |
| Jul 2022 – Nov 2025 | Backend Software Engineer (.NET) | 3 y 4 m |
| Nov 2025 – present | Senior Software Engineer · Technical Leader | — |
Thirteen years at the same group, four of them as an engineer. First commit on 10 August 2022. In the four years since, the scope grew in three steps: from a feature, to a whole service with back and front, to platform and standards other teams consume.
Education — Postgraduate lato sensu in Web Development (UNIFAEL) · Technologist in Systems Analysis and Development (UNINTER). Six certifications between October 2021 and December 2022, concentrated in the career transition.
Novo Hamburgo, RS · Brazil

