I’m Abe. I engineer and deliver the web systems that support the California Senate, Assembly, and Office of Legislative Counsel. I operate on a core conviction: technology must align with the business it serves. I leverage my business acumen to transform intricate requirements into the production-ready software and interfaces that provide daily operational value to the Legislature.
A decade in the mission-critical, high-velocity environment at the State Capitol has taught me what actually matters: clear requirements, sound architecture, and owning what you ship. I build for longevity, ensuring that the solutions I deliver provide lasting value to the legislative process.
02 // THINKING
Software is more than its syntax. These perspectives document the mental models and architectural logic I apply to every system I build.
Responses drawn from a decade of real-world development.
system design
The first question organizations ask when implementing generative AI is which model is safe. That is the wrong question, and answering it wastes months. An open-weight model can run inside a restricted network boundary or on a public cloud. The model weights are identical either way. The environment determines whether sensitive records can exist in the request at all.
Architecture must follow data classification. The initial step is identifying what data exists, determining the sensitivity of each tier, and defining the processing requirements for that sensitivity. I designed a system that supports two hosting postures behind a single interface. Sensitive records stay strictly on-premises where the inference tier has no outbound internet access. Public content routes to a managed cloud for elastic scale. The application talks to one endpoint and does not care which environment answered.
Choosing a model before defining these boundaries forces you to retrofit policy onto an existing tool. That is an expensive mistake.
Every model request and response transits a single gateway. There are no exceptions and no alternate routes. This gateway executes the non-negotiable work: request guardrails, sensitive data redaction, audit logging, and providing a unified API surface.
Applications never communicate with the inference tier directly. An advisory control in a regulated environment is not a control. Forcing all traffic through a mandatory hop converts a written policy into a verifiable technical mechanism. Early designs routed generated output directly back to the application. That left output guardrails and audit trails covering only half the interaction. Routing the response back through the gateway corrected that asymmetry.
When a system retrieves evidence to answer a prompt, the access filter must execute during assembly, not after generation. Once unauthorized data enters the context window, the security boundary is compromised. You cannot instruct a model to forget what it just read.
The filter must fail closed. If the user lacks clearance or the retrieved evidence scores below a relevance threshold, the system issues a specific refusal and directs the user to an authoritative source. Fabrication is treated as a systemic defect requiring a root cause analysis, not a prompt engineering issue. Permissions continuously resynchronize with the identity system to prevent access drift.
The inference tier runs on a standard API shape backed by open-weight models. Swapping models is a configuration change. No application code is written against a specific vendor SDK.
This prevents architectural lock-in. An organization that must rewrite applications to change model providers has acquired unquantifiable technical debt. The principle is identical to managing a storage backend: own the interface, rent the implementation, and maintain control over the integration seam. High availability is reserved for the inference tier where failures are expensive. Undifferentiated redundancy across the entire stack turns a prototype into an unapproved budget.
Honesty about system maturity is an architectural requirement. Request-side guardrails are actively running in the integration layer. Response-side controls, output validation, and citation enforcement are specified for the prototype phase. A system with request-side guardrails only can still generate incorrect outputs. Claiming the architecture is complete misrepresents the current risk posture.
If you are building an AI platform, classify the data and finalize the network boundary before evaluating a single model. That decision dictates whether the system is viable for actual organizational work, and it is the only decision that is difficult to reverse. Every subsequent choice regarding the model, the vector store, or the serving runtime is just a component behind an interface.
Banh Mi Ops
An AI agentic orchestration framework built for Claude Code. It turns a single AI session into a coordinated team of specialized agents, managed through a real-time kanban dashboard. You describe what you want built. It scouts your codebase, writes the development plan, and deploys agents in parallel to execute it. The underlying architecture is built on engineering principles that have worked for decades across disciplines: separation of concerns, dependency-driven scheduling, independent verification, and bounded revision cycles.
Why "Banh Mi"? A great bánh mì is its own kind of orchestration. Every layer has a role, the timing matters, and if one ingredient is off the whole thing falls apart. I like codenames, and I'd rather name a framework after something I genuinely love.
What It Does
You point it at a codebase. It runs reconnaissance, maps dependencies and conventions, then generates a development plan. Tasks that can run in parallel do. Harder decisions get smarter models. Every piece of work gets independently verified before it ships.
Pipeline
Two execution modes: Oversight pauses for your approval at each step. Autonomous runs the full wave without stopping.
Why I Built It
When chat-based AI tools became publicly available in 2022, the technology was premature but the potential was obvious. I started exploring how these tools could be shaped around the engineering principles I've built my career on: reconnaissance before action, independent verification, bounded revision, and clear separation of concerns. Banh Mi Ops is that exploration turned into a working framework. For more on this trajectory, see Context Over Prompts.md.
From service architecture to cache optimization to patterns shared across a codebase, my aim is to document thoroughly by default. Meaningful comments, source-linked references, docblocks that explain why rather than what. I learned every tool here by hand before automating any of it, from Git on the CLI to Drupal’s service container.
With AI tools available today, I leverage them to streamline that workflow. Commits land with the right structure in fewer tries. Tests generate against real method signatures. Sibling files that would have waited get updated in the same session. The code samples below show what that looks like.
The walkthrough below is interactive and faster than reading the repo directly. I would recommend starting here and using the repo link above when you want to inspect a file end to end.
Each section shows two versions of the same file: BY HAND and WITH AI. On a wide screen they sit side by side. On a phone, tap between BY HAND and WITH AI at the top of each section to swap the code in place. Highlighted bands mark regions worth comparing. Hover or tap a highlighted region to focus it and read the annotation explaining what changed and why. The rest of the code dims so the comparison is clear. Use the Tour toggle above each section to switch the annotations off and read the code on its own.
The BY HAND side is how I write under strict project deadlines: typed signatures, a detailed docblock on the public method, and a TODO where I knew the constructor refactor was the right call but not the priority. It ships. It works. The WITH AI side is what my AI workflow produces when I pass that same service through it: declare(strict_types=1), constructor injection, full PHPDoc on every method, an extracted interface for the test harness, and a Kernel test stub covering the defaults. The logic did not change. The documentation and testability surface did.
BY HANDphp/manual/AppThemeService.php
<?phpnamespace Drupal\app_theme_toggle\Service;class AppThemeService { /** * Get the user's theme toggle state. * * @param int $userID * The user ID. * * @return string * The user's theme toggle state. Defaults to "light-theme" when the * user has never toggled the preference. */ public function getUserThemeState(int $userID): string { // TODO: inject user.data through the constructor when we touch this // class again. Calling \Drupal::service() works but hides the // dependency. $userData = \Drupal::service('user.data'); return $userData->get('app_theme_toggle', $userID, 'theme_state') ?? 'light-theme'; }/** * Set the user's theme toggle state. */public function setUserThemeState(int $userID, string $themeState): ?string { try { $userData = \Drupal::service('user.data'); $userData->set('app_theme_toggle', $userID, 'theme_state', $themeState); // Invalidate the cache so the next page render picks up the change. $tag = 'user:' . $userID . ':theme_state'; \Drupal::service('cache_tags.invalidator')->invalidateTags([$tag]); return $themeState; } catch (\Throwable $e) { \Drupal::logger('app_theme_toggle')->error($e->getMessage()); return NULL; }}// helper, used internallyprotected function buildCacheTag(int $userID): string { return 'user:' . $userID . ':theme_state';}}
48 lines
01 / 02BY HANDThe service ships and works. Typed signatures, a full docblock on the public method, and clear separation between the getter and setter. What it does not have: strict_types, constructor injection, or an interface. Those were scoped out because the refactor would have touched the service definition YAML, every existing test mock, and required a new interface file. Under strict project deadlines, shipping the feature took priority over the ceremony around it.
02 / 02BY HAND\Drupal::service() is Drupal's service locator. It works, but it hides the dependency from the constructor signature, which means the class cannot be instantiated in a test without bootstrapping the full container. I knew this when I wrote it. The TODO names the exact refactor, the reason it matters, and what the next engineer needs to do. That specificity is intentional: a TODO that says "fix this later" helps nobody.
WITH AIsrc/Service/AppThemeService.php
<?phpdeclare(strict_types=1);namespace Drupal\app_theme_toggle\Service;use Drupal\Core\Cache\CacheTagsInvalidatorInterface;use Drupal\Core\Logger\LoggerChannelFactoryInterface;use Drupal\user\UserDataInterface;class AppThemeService implements AppThemeServiceInterface {public function __construct( protected UserDataInterface $userData, protected CacheTagsInvalidatorInterface $cacheTagsInvalidator, protected LoggerChannelFactoryInterface $loggerFactory,) {}/** * Get the user's theme toggle state. * * Defaults to "light-theme" when the user has never toggled. * * @param int $userID * The user ID. * * @return string * The active theme state string. */public function getUserThemeState(int $userID): string { return $this->userData->get('app_theme_toggle', $userID, 'theme_state') ?? 'light-theme';}/** * Set the user's theme toggle state and invalidate the render cache. * * @param int $userID * The user ID. * @param string $themeState * The theme state to persist (e.g. "dark-theme"). * * @return string|null * The persisted state, or NULL on failure. */public function setUserThemeState(int $userID, string $themeState): ?string { try { $this->userData->set('app_theme_toggle', $userID, 'theme_state', $themeState); $this->cacheTagsInvalidator->invalidateTags([$this->buildCacheTag($userID)]); return $themeState; } catch (\Throwable $e) { $this->loggerFactory->get('app_theme_toggle')->error($e->getMessage()); return NULL; }}/** * Build the per-user cache tag for theme state. * * Centralised so the REST resource and the kernel test invalidate the * same string. * * @param int $userID * The user ID. * * @return string * A cache tag in the form "user:{uid}:theme_state". */protected function buildCacheTag(int $userID): string { return 'user:' . $userID . ':theme_state';}}
73 lines
01 / 02WITH AIDuring a pass through with AI, the class gained constructor injection via three typed interfaces, strict_types at the file level, and full PHPDoc on every method. It now implements AppThemeServiceInterface, making it testable in isolation without bootstrapping Drupal. Also generated in the same session: the interface file and a Kernel test stub covering the default theme state.
02 / 02WITH AIThe AI agent replaced the service locator with $this->userData, injected at construction through UserDataInterface. The dependency is declared in the constructor signature, typed against an interface, and follows dependency inversion. Any test can now instantiate this service with a mock and no container. The TODO is gone because the refactor is done.
Same logic, same file. Also generated in this session: AppThemeServiceInterface.php, tests/Kernel/AppThemeServiceTest.php (3 cases).
The docblock that explains the session-messenger leak is entirely hand-written, and it is the most valuable thing in the file. The code fix is small. What I did not have time for was the regression test covering both branches of the _wrapper_format check, or the CLAUDE.md addendum so the next session does not have to rediscover the raw-query-string gotcha. The WITH AI side has both. The diagnosis is unchanged. The coverage around it is the agent’s contribution.
BY HANDsrc/EventSubscriber/AppModalMessengerSubscriber.php
<?phpdeclare(strict_types=1);namespace Drupal\app_modal_ui\EventSubscriber;use Drupal\Core\Messenger\MessengerInterface;use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Component\HttpKernel\Event\ResponseEvent;/*** Clears session messenger for modal AJAX responses.** When forms are submitted inside a modal dialog, Drupal's form validation* and submission handlers add messages to the session-based messenger.* These messages are normally consumed on the next full page render, but* in a modal AJAX context they are never consumed, causing them to "leak"* and appear unexpectedly on the next page the user visits.** This subscriber runs after the response is built and clears any* remaining session messages for modal requests. The messages are already* delivered to the client via AJAX MessageCommand and shown in the modal* by the JS layer.*/class AppModalMessengerSubscriber implements EventSubscriberInterface {public function __construct( protected MessengerInterface $messenger,) {}public function onResponse(ResponseEvent $event): void { // Only act on the main request, not sub-requests rendered through // the kernel (BigPipe, ESI, etc.). if (!$event->isMainRequest()) { return; } $request = $event->getRequest(); // Pull the wrapper format from the query string. drupal_modal is set // by the dialog open call, drupal_ajax is what ajax.js switches to // when a form rebuilds. $wrapperFormat = $request->query->get('_wrapper_format'); // Case 1: original modal open request. if ($wrapperFormat === 'drupal_modal') { $this->messenger->deleteAll(); return; } // Case 2: AJAX form rebuild from a modal context. The wrapper format // is now drupal_ajax, but the raw QUERY_STRING still carries the // original drupal_modal value. if ($wrapperFormat === 'drupal_ajax') { $rawQuery = $request->server->get('QUERY_STRING', ''); if (str_contains($rawQuery, '_wrapper_format=drupal_modal')) { $this->messenger->deleteAll(); } }}}
60 lines
01 / 01BY HANDThis docblock explains a bug the code alone cannot reveal. When a form submits inside a modal dialog, Drupal's messenger queues status messages into the session. In a modal AJAX context they never get consumed, so they leak onto the next page the user visits. I traced this through the messenger service, the AJAX response pipeline, and the raw QUERY_STRING edge case. Every sentence here is from that investigation. The fix shipped. The regression test did not. Under strict project deadlines, the subscriber worked and I moved on.
WITH AIsrc/EventSubscriber/AppModalMessengerSubscriber.php
<?phpdeclare(strict_types=1);namespace Drupal\app_modal_ui\EventSubscriber;use Drupal\Core\Messenger\MessengerInterface;use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Component\HttpKernel\Event\ResponseEvent;/*** Clears session messenger for modal AJAX responses.** When forms are submitted inside a modal dialog, Drupal's form validation* and submission handlers add messages to the session-based messenger.* These messages are normally consumed on the next full page render, but* in a modal AJAX context they are never consumed, causing them to "leak"* and appear unexpectedly on the next page the user visits.** This subscriber runs after the response is built and clears any* remaining session messages for modal requests. The messages are already* delivered to the client via AJAX MessageCommand and shown in the modal* by the JS layer.*/class AppModalMessengerSubscriber implements EventSubscriberInterface {public function __construct( protected MessengerInterface $messenger,) {}public function onResponse(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } $request = $event->getRequest(); $wrapperFormat = $request->query->get('_wrapper_format'); if ($wrapperFormat === 'drupal_modal') { $this->messenger->deleteAll(); return; } // During AJAX form rebuilds, Drupal's ajax.js changes the wrapper // format to drupal_ajax, but the original drupal_modal is still in // the raw query. if ($wrapperFormat === 'drupal_ajax') { $rawQuery = $request->server->get('QUERY_STRING', ''); if (str_contains($rawQuery, '_wrapper_format=drupal_modal')) { $this->messenger->deleteAll(); } }}}
54 lines
01 / 01WITH AIWith AI in the loop, the diagnosis stayed untouched and the missing coverage shipped in the same session. A unit test now covers both branches of the _wrapper_format check: the direct drupal_modal case and the AJAX rebuild edge case where the wrapper format changes mid-request. A CLAUDE.md addendum documents the raw-query-string behavior so the next engineer or agent session starts with context instead of rediscovery.
Same diagnosis, same file. Also generated: tests/Unit/AppModalMessengerSubscriberTest.php (2 cases), CLAUDE.md 'Gotchas' addendum.
I extracted the trait. That was my call: three copies of the same isModal() check across sibling form classes, and the fourth copy made it a refactor I owed the next engineer. The BY HAND side is what I would realistically ship solo under strict project deadlines: one form cleaned up, four siblings still inlined, trait saved for “next sprint.” The WITH AI side is the same extraction propagated to all five forms, with each form’s $modalSuccessMessage set correctly, the trait file tested, and a README section added so the pattern is discoverable without reading the trait.
01 / 02BY HANDSix use statements importing AJAX command classes that this form builds by hand: AjaxResponse, CloseModalDialogCommand, MessageCommand, RedirectCommand, plus FormBase and FormStateInterface. Every sibling form in this module carries the same six imports. The fourth copy is what crossed the threshold from acceptable repetition to a refactor I owed the codebase. Under strict project deadlines, I extracted the trait from this one form and left the other four for next sprint.
02 / 02BY HANDThe AJAX submit handler builds the response manually: close the modal dialog, show a status message, read the destination from the query string, redirect. Three commands, same order, same logic in every sibling form. The only variation is the message string. Under strict project deadlines, this was the form I cleaned up. The other four still carry their own hand-built versions.
WITH AIsrc/Form/RecordEditForm.php (after trait extraction)
01 / 02WITH AIOne pass through my AI workflow collapsed six imports to two: FormBase and AppModalTrait. The trait encapsulates the full modal AJAX pattern behind a clean API. Net result: thirty duplicated lines across five sibling forms became five one-line trait inclusions, each declaring only its own $modalSuccessMessage.
02 / 02WITH AIThe agent propagated the extraction to all five forms and replaced each hand-built response with a single method call. It set each form's success message, wrote trait-level tests, and added a "Modal forms" section to the README. Work that would have taken three or four focused sprints to schedule landed in one session.
Same form, same extraction. Also propagated: 4 sibling forms, AppModalTrait tests, README 'Modal forms' section.
My natural mode is verbose. When I write a comment, it usually explains why, not what, and when the why depends on a workaround I read somewhere, I include the source: a Stack Overflow answer, a Drupal.org issue, a GitHub issue, the README of the offending module. The next engineer on the file should be able to walk the same path I walked. Commits work the same way. Subject line says the change. The body says the reason and links the artifact that informed it. I would rather over-explain in writing than send someone hunting for context that lives in my head.
Contrary to popular convention, I do not write strict Conventional Commits, and on that point I share the reasoning laid out in this post by a fellow engineer . The format was designed to make changelog automation easier in an era when humans were the only readers of the log. My one carry-over from the convention is that commits always have a body explaining the why. With AI in the loop, a verbose body is more useful, not less: it costs more tokens but it gives the next session (and the next reviewer) a real anchor when something breaks. The accuracy is worth the tokens.
The discipline does not change. The execution gets faster. With an AI agent in the loop I get better commit subjects in fewer tries, bodies that hit the why without padding, and PHPdoc blocks generated against the actual signature and the actual surrounding pattern instead of a template. References to the Stack Overflow answer, Drupal issue, or in-repo .md spec stay verbatim because they are the load-bearing parts of the commit. The agent does not write the source citation; it makes sure I do not lose it during cleanup.
BY HANDgit log (manual commit)
Refactor theme toggle to use constructor injectionThe service was calling \Drupal::service('user.data') inside themethod body, which hides the dependency from the constructorsignature and makes the class untestable without bootstrappingthe full container.Moved to constructor injection with UserDataInterface. Alsoadded strict_types and a cache tag helper.See: https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection
11 lines
WITH AIgit log (AI-assisted commit)
Refactor AppThemeService to constructor injectionReplace \Drupal::service('user.data') calls with constructor-injected UserDataInterface. The service locator hid thedependency from the signature and prevented unit testingwithout a full container bootstrap.Changes:- declare(strict_types=1)- Constructor injection: UserDataInterface,CacheTagsInvalidatorInterface, LoggerChannelFactoryInterface- Extract AppThemeServiceInterface for test doubles- Add Kernel test covering default theme state fallback- Centralize cache tag builder with docblock explainingwhy the tag string is shared across REST + testRef: https://www.drupal.org/docs/drupal-apis/services-and-dependency-injectionRef: https://www.drupal.org/project/drupal/issues/2142515
Below is a sample CLAUDE.md (for Codex, AGENTS.md). In practice, the real files are more advanced depending on the project. This one covers the basics: the stack, the conventions, the boundaries around what the agent should and should not do, and the rules around destructive actions.
CLAUDE.md (representative project guardrails)
AGENT GUIDE
CLAUDE.md
Read this before touching code in this repo.
Stack
PHP 8.2+, Drupal 10/11, vanilla JS (no framework), SCSS, Twig, YAML.
PSR-12 / Drupal coding standards. Strict types on all new PHP.
Code conventions
Names carry the meaning. If you write a comment to explain a name,
rename it instead.
Functions are short and single-responsibility. Extract once a function
exceeds ~30 lines or holds two unrelated branches.
Inject dependencies through constructors. Do not call
`\Drupal::service()` from inside business logic in new code.
New PHP files declare strict types and use typed properties with
constructor property promotion.
Comments and documentation
Default to writing no comment.
Add a comment when why is non-obvious: a hidden constraint, a
subtle invariant, a workaround for a specific bug, behavior that
would surprise a future reader.
Do not restate what the code does. The reader knows the language.
Do not track the current task, fix, or ticket in code. That belongs
in the commit message.
Every public method and class gets a docblock describing purpose and
contract.
Commits
Subject line is imperative, under 70 characters.
Body is for why, not what. The diff shows what.
One sentence per commit. If the message needs paragraphs, the commit
is too big. Split it.
What you should help with
Boilerplate (routing YAML, service definitions, plugin scaffolding).
Mechanical refactors (renames, extractions, threading a parameter
through).
First-pass implementation of a clearly-specified design.
Tedious type-juggling and PHPStan-style cleanup.
What you must defer to me on
The shape of the solution: what classes exist, what they expose.
Architectural decisions: trait vs base class, event subscriber vs
hook, when to extract a service.
The diagnosis of bugs. Propose fixes after I describe the diagnosis,
not before.
The "why" of any comment that ends up in the code.
Git safety
Never run `git reset --hard` while the working tree has uncommitted
modifications outside the files intended to discard.
Never push without explicit instruction.
Never skip pre-commit hooks (`--no-verify`). If a hook fails, fix
the underlying issue.
# CLAUDE.mdRead this before touching code in this repo.## StackPHP 8.2+, Drupal 10/11, vanilla JS (no framework), SCSS, Twig, YAML.PSR-12 / Drupal coding standards. Strict types on all new PHP.## Code conventions- Names carry the meaning. If you write a comment to explain a name,rename it instead.- Functions are short and single-responsibility. Extract once a functionexceeds ~30 lines or holds two unrelated branches.- Inject dependencies through constructors. Do not call\`\\Drupal::service()\` from inside business logic in new code.- New PHP files declare strict types and use typed properties withconstructor property promotion.## Comments and documentation- Default to writing no comment.- Add a comment when *why* is non-obvious: a hidden constraint, asubtle invariant, a workaround for a specific bug, behavior thatwould surprise a future reader.- Do not restate what the code does. The reader knows the language.- Do not track the current task, fix, or ticket in code. That belongsin the commit message.- Every public method and class gets a docblock describing purpose andcontract.## Commits- Subject line is imperative, under 70 characters.- Body is for *why*, not *what*. The diff shows what.- One sentence per commit. If the message needs paragraphs, the commitis too big. Split it.## What you should help with- Boilerplate (routing YAML, service definitions, plugin scaffolding).- Mechanical refactors (renames, extractions, threading a parameterthrough).- First-pass implementation of a clearly-specified design.- Tedious type-juggling and PHPStan-style cleanup.## What you must defer to me on- The shape of the solution: what classes exist, what they expose.- Architectural decisions: trait vs base class, event subscriber vshook, when to extract a service.- The diagnosis of bugs. Propose fixes after I describe the diagnosis,not before.- The "why" of any comment that ends up in the code.## Git safety- Never run \`git reset --hard\` while the working tree has uncommittedmodifications outside the files intended to discard.- Never push without explicit instruction.- Never skip pre-commit hooks (\`--no-verify\`). If a hook fails, fixthe underlying issue.
The CLAUDE.md above is the foundation. I also use a library of custom skills and workflows that are not listed here, but one thing stays constant: I review every Merge Request before accepting the code. Generated code I do not understand does not pass the MR.
For a deeper look at how I arrived at this workflow and how I think about AI in engineering, see Context Over Prompts.
Two deliberate omissions worth naming. Testing is a continuous process I am still refining, and the test files in the source repo do not represent my current practice as well as the production code does. The module and class names you see here are redacted from the originals so they read as a pattern rather than a specific org’s product surface. I am happy to walk through any file in the repo in an interview.
Across the California State Assembly, offices and committees order what they need through one commerce platform, reaching all 80 districts, their satellite offices, and 80+ committees. They order across distinct domains: Furniture, Supplies, Equipment, Telecom, Print, Ergonomic, and Special, each with its own catalog, its own approvals, and its own definition of a complete request. The earlier system it replaced had run for over a decade, carrying nearly 100,000 records of order history. I led the rebuild over about four months with a small team: another backend engineer and a front-end developer who also handled the design. The result is one modern commerce platform on Drupal Commerce, one system underneath, a storefront for every department on top, no two alike.
02 — ONE CORE, MANY STOREFRONTS
Every department runs its own store, with its own catalog and its own rules, but underneath they all share a single core I built. Seven ordering domains, from furniture to telecom, run through one order model and one fulfillment lifecycle. The hard part was letting every department differ that much without the code splitting into a pile of special cases, and the shared core is what kept that from happening. It also keeps the platform easy to grow: a new department, or a new kind of order, extends the core once instead of becoming its own separate build.
03 — CHECKOUT, BUILT PER STORE
Checkout had to vary by store, because ordering office supplies is nothing like arranging a furniture delivery. Most domains share one multi-step checkout I shaped with our front-end developer, who also handled the design, so each store speaks its own language in its labels, contact pane, and summary while running the same proven path. Physical goods need more than a request, though: furniture and equipment can involve a scheduled delivery, or a pickup to haul away the old or broken item, so those flows add a step for the dates and details the others never need.
04 — THE ADMIN SIDE
Most of the system's weight sits on the admin side, where each department works a queue scoped to only the orders it owns. Who can order is just as controlled: each person orders only for the offices and committees they are cleared for, and only from the stores they are permitted to use, which across 80 districts and 80+ committees is its own problem to get right. Every order moves through one fulfillment lifecycle, from request to review to fulfillment, identical across every domain, so a manager who learns one queue understands them all. I kept this coherent as it grew by running code reviews and pairing with engineers through the harder parts of the order model. Above the queue sits a reporting layer that turns order history into something departments plan against: cost by district and office, forecasting, and materials and inventory. The same history that drives the queues is what each department reads to plan ahead.
05 — MIGRATION & IMPACT
Nearly 100,000 records of order history had to come across from the legacy system. I led the migration as a staged pipeline: extract, map, validate before load, then verify against the source, so the new system started on data it could trust.
Two outcomes mattered to me as much as the engineering did. First, the Assembly wanted a modern, cohesive experience for staff, and advanced features the older system could not support. The new platform delivered both: storefronts that feel like one modern, polished product, and the foundation for features that were out of reach before. Second, the architecture is matched to the Assembly's business process, so as offices, committees, and their workflows change, the platform adapts with them instead of needing another rebuild. Understanding how the Assembly actually operates was as much the job as writing the code. Since launch, the Assembly has gained a range of new capabilities with little friction from the old system, and its full order history was preserved intact.
Three constraints decided almost everything about this site, and I set them before writing any code.
It has to load immediately, because a visitor deciding whether to keep reading will not wait. One person has to be able to maintain it, because that person is me and I have a job. And it has to survive being read by engineers, which rules out anything I cannot explain.
Each decision below follows from those, and each one had an alternative I rejected for a reason I can still give you. That is the part worth reading. The stack itself is unremarkable.
There is also a live AI search on this site, answering from a corpus of my own work, with its own guardrails and evaluation harness. That is a running service, not a widget, and keeping it from turning the whole site into a framework project was the hardest constraint to hold.
The design system
I chose IBM's Carbon Design System, Gray-100 dark theme because it gave me a complete set of rules: spacing scale, type scale, color tokens, grid. That removed hundreds of small decisions I didn't want to make.
The trade-off is that it's austere. Seven colors, two typefaces (IBM Plex Sans and Plex Mono), no gradients, no shadows. The visual identity comes from typography weight and spacing rhythm, not decoration. The one override is the accent color: Carbon's default interactive blue replaced with #50FFB4, a mint green carried over from my previous portfolio. It cascades through every interactive state, link, and label. One color, one job: signal what's active or important.
7
Colors
2
Typefaces
1
Accent
0
Box shadows
The framework
This is a content site. Most of it is static text and layout. The question was: how much JavaScript does that actually require?
Astro's answer is zero by default. Every .astro component compiles to HTML at build time. The framework removes itself from the output. Interactive components opt in explicitly with client:load, creating what Astro calls "islands": isolated React components that hydrate independently. That table in the source comment above is the full list. Three React islands. Everything else was finished rendering before the browser's JavaScript engine initialized.
I could have built this in Next.js or Remix. I've used both. But they ship a client runtime whether you need it or not. For a site where 90% of the content is static prose, that felt like paying a tax for convenience I wasn't using. Astro let me keep the component model (composability, props, slots) without the runtime cost.
The content layer
Every piece of long-form writing on this site is an artifact. They live as .mdx files in a content collection with typed frontmatter validated at build time by a Zod schema. If a field is wrong, the build fails. The system supports two rendering paths: standard MDX for prose, and custom React components for interactive content. The Code Samples artifact renders side-by-side code comparisons with inline annotations. Banh Mi Ops has an animated pipeline diagram. Both paths flow through the same ArtifactBody.astro wrapper.
Featured artifacts also render in an off-canvas side panel on the home page. One shared component, two contexts, scoped CSS adjustments for the narrower panel column.
The build pipeline catches a few other things. A custom Astro plugin strips font preload links because Astro's default preloading conflicts with @fontsource. A post-build script verifies the Pagefind search index generated correctly. And the glossary system (inline definition tooltips you'll find on some artifacts) has the strictest check: every definition lives in a single TypeScript registry, referenced by slug. If the slug doesn't exist, the build fails.
Other decisions
A few choices that don't fit neatly into the sections above but shaped the site just as much.
Static HTML
Nothing on this site changes per-request. Static output means the CDN cache is the feature, not a workaround.
One accent color
Fewer colors means each one carries more weight. Mint signals 'active' or 'important' because nothing else competes.
Pagefind
Client-side search index built at compile time. No external service, no API key, no monthly cost.
SQLite
The admin portal has one user. A file-based database is the right tool for that scale.
No template
Every component is hand-built on Astro, with IBM Carbon as the design foundation. No portfolio theme or page builder underneath, so every layout and interaction is a choice I can explain.
One thing I did for my own amusement: the home page opens with a note in the <body> tag.
Good morning. Or afternoon. Or evening.
This is static HTML so I genuinely don't know
what time it is for you. But hi. Abe here.
You're inspecting a personal site. I respect that.
Let me walk you through what's under the hood.
The comment continues for about 60 lines. It explains the framework, the architecture, why CSS animations run the hero instead of JavaScript, and ends with a table:
┌─────────────────┬────────┬────────────────────────┐
│ Component │ Type │ Why it needs JS │
├─────────────────┼────────┼────────────────────────┤
│ ResumeModal │ React │ open/close + animation │
│ SkillsSpotlight │ React │ SSE streaming + state │
│ SkillsTrigger │ React │ keyboard shortcuts │
├─────────────────┼────────┼────────────────────────┤
│ ArtifactPanel │ .astro │ CSS + DOM only │
│ SelectionTooltip │ .astro │ CSS + DOM only │
│ ContactModal │ .astro │ CSS + DOM only │
└─────────────────┴────────┴────────────────────────┘
3 islands. That's the entire JS footprint.
The rest was finished before your browser woke up.
As you scroll through the markup, each section has its own note:
HeroYou opened it. Welcome to the markup.
AboutStill here? Keep going.
ThinkingMost people never look this deep. You're not most people.
CareerYou're thorough. That's rare. I respect it.
FooterYou made it. Try abe.contact() in the console.
And the console has an API:
> abe.ai("your question") ask me anything, streamed to console
> abe.search() open the AI spotlight (or hit ⌘K)
> abe.contact() open the contact modal
None of that is load-bearing. It is there because the people most likely to open the source are the people I would want to talk to.
That's how it's built
I built this with Claude Code, the same tool I wrote Banh Mi Ops to orchestrate, using the methodology described in the other artifacts here. The site is the working example of the practice rather than a description of it.
The test I hold this to is whether every choice on the page has a reason I would defend in review, and a rejected alternative I can name. Three React islands rather than a React site. A design system rather than a visual identity I invented. A file-based database because there is one user. None of those are clever. They are just decisions someone made on purpose, which is the whole standard.
Data migration in government infrastructure demands replacing a foundation while people are still working inside the building. We handle authoritative records. A dropped row represents a permanent failure of public record, and deferred cleanup never survives contact with a system people rely on to do their jobs. The standard for success requires a decade of historical records to remain perfectly intact inside a database that models the world completely differently.
I recently led a migration of nearly 100,000 records across nine departments, moving them from a legacy framework into a modern, unified platform. Nine departments had spent years developing completely independent workflows, approval chains, and catalog structures. The mandate was to map all of that inherited complexity into a single shared core.
Getting the core architecture wrong means the old data cannot make the trip. Getting the mapping wrong means the new system grows a custom exception for every department, immediately rebuilding the exact silos the consolidation was built to eliminate. Designing the architecture and writing the migration are the exact same problem viewed from different angles.
A terminal output reporting “success” only confirms the script did not crash. To prove a migration is actually complete, you have to assume it failed and force the system to prove otherwise. We established completeness through three strict validation gates, each designed to catch a failure the previous gate was blind to.
Absolute row counts. This cheap check catches the loud failures, ensuring no records drop because a dependency ran in the wrong order.
Field-level reconciliation. A record can migrate with a critical field wiped blank by a mismatched transform while easily passing a simple row count. We compared the source and destination contents directly until every single discrepancy was explained.
Workflow validation. The teams who own the data exercised their daily procedures against the migrated content before we switched anything over. Values can map perfectly, but if an old approval routes to the wrong reviewer in the new system, the migration fails. This gate catches the organizational logic that no database query can ever see.
When it came time to launch, a single switch-flip would have made a rollback plan the only thing standing between us and nine simultaneous departmental crises. We staged the rollout department by department, running the old and new platforms in parallel. Staging turned one unmanageable risk into nine small, inspectable ones, allowing the new platform to earn the trust of the people relying on it.
The most interesting engineering in a migration happens in the custom mapping logic. The work that actually determines success is the rigorous reconciliation nobody sees. Migrating legacy systems lacks the visibility of a new product launch, as the highest measure of success is a transition the users never notice.
I view this work the way structural engineers approach moving a historic building intact. You carefully lift the entire weight of the institution, preserve the exact integrity of the spaces where the real work happens, and lower it onto a modernized foundation. Modernizing government technology requires building an architecture resilient enough to carry that history forward.
interactive
A note first
Most of what I have built is closed source, so I cannot show the source code itself. These numbers are what I can show of it. They do not measure quality or performance. They measure frequency: how often the work shipped, and the busy stretches of my career, across almost ten years.
These come straight from GitLab Analytics: the commits and merge requests I open, review, and merge, going back to my first month in 2016. The busy months were busy for a reason. Demand from our customers ran high, deadlines landed back to back, and at the peak I had six or seven projects running at once.
The chart below lets you explore it month by month, by commits or merge requests.
The shape of the work
Each cell is one month; brighter means more shipped. The early stretch is thin because that is where I started, as a student assistant still finding my footing. Hover those first months and the role shows.
Monthly contributions
2016-202618,491 commits total · hover any month
J
F
M
A
M
J
J
A
S
O
N
D
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
LessMore
Yearly commits and merge requests authored, 2016 to 2026
Year
Commits
Merge requests authored
2016
64
1
2017
291
127
2018
1498
512
2019
742
283
2020
475
144
2021
1083
277
2022
2075
710
2023
3411
1261
2024
2265
827
2025
3103
1012
2026
3484
942
A few months show no recorded activity, whether genuinely quiet or simply outside what this count captures. Data as of 2026-05-30; the current month is partial.
How these are counted
Commits are the ones I authored.
Merge requests are the ones I opened, that were reviewed, and merged.
What the numbers leave out
What these numbers cannot show is whether the work was sound, whether it aged well, or whether the next person can pick it up. Most of the code is closed source, so the closest I can show is this:
Code Samples puts code I wrote by hand next to the same work after an AI-assisted session, so you can judge the craft directly.
Web Data Services is a system I led to production that other teams still build on, which is the kind of longevity these numbers cannot show.
I have built and maintained systems for the California State Legislature for over a decade. The pace of this work is faster than most assume, and the stakes are public record. The principles below were forged in that environment. They are not theoretical best practices. They are the result of maintaining mission-critical applications and learning from the friction that only surfaces years after a system goes live.
I still maintain systems I architected at the beginning of my career. In an organization where applications evolve over decades, that continuity teaches you lessons that reading about maintainability never could. You discover exactly which abstractions held up under real-world pressure and which became obstacles everyone works around.
That experience compounds. I build every system with a strict assumption: anyone on the team must be able to maintain it five years from today. Sustainability is a shared responsibility. The best way to future-proof an architecture is ensuring no single engineer has to carry it alone.
I have worked with Drupal my entire career. It provides authentication, role-based access control, and session management out of the box. These are solved problems, and I treat them that way. I lean on foundational frameworks so I can spend my engineering hours where they actually matter: the complex domain logic that requires institutional knowledge to execute correctly.
Engineering for the legislative process requires understanding the lifecycle of a bill, the urgency of floor sessions, and the mechanics of multi-department workflows. The most valuable technical decisions I make come from knowing exactly which problems deserve my time and having the discipline to outsource the rest.
The systems supporting legislative operations carry heavy institutional complexity. This is essential complexity. It lives in the problem domain and cannot be designed away. It can only be managed well or managed poorly.
The part I control is the accidental complexity. I actively prevent the codebase from accumulating unnecessary abstraction layers or clever patterns that obscure intent. Clever code is a liability. A new developer should be able to trace a request through the system without a guided tour. I aim for architectures robust enough to handle the institutional weight, but clear enough that the next person can work in them confidently.
A system is only as trustworthy as the automated checks gating its release. For the AI features I build, this requires a rigorous evaluation harness. Representative queries live in fixture suites, including explicit attempts to break the system through prompt injection or scope extraction. A failure stops the release rather than generating a ticket.
The rule is simple: every correct refusal becomes a permanent fixture. When the system declines a prompt it should have declined, that exact shape goes into the suite so a future update cannot silently undo it. When it fails to decline, we treat it as a defect with a root cause, not a prompt engineering issue. Under fixed legislative deadlines, I do not always write the test first. But I absolutely demand a definition of “correct” and an automated way to verify it before shipping.
When a production system breaks during a legislative session, recovery depends entirely on whether the system can be questioned. This requires structured logs that tell a coherent story, error paths that surface detail instead of swallowing it, and decisions encoded clearly enough for a reader to follow without insider knowledge. Auditability and traceability are non-negotiable properties of mature production engineering.
A system that someone else can maintain five years from now is the exact same system you can debug under pressure today. Both rely on a codebase designed deliberately for the people who will inherit it.
system design
01 — CONTEXT
On the California State Senate Floor, the team supporting Floor Sessions keeps track of floor attendance as a session runs. That tracking is critical: an accurate quorum is needed before a Floor Session can begin. At the time, that happened the manual way: status was called in over the radio and logged on paper, a process that was hard to audit, slow to report on, and easily out of date. They needed it digitized into a real-time system across multiple tablets, where a change made on one device appears on all of them immediately.
Because attendance lived across separate radios and each person's own notes, no two of them held the same picture. A status known to one of them reached the others only when it was called in and written down, so the records were always a step behind. That delay was the gap the system was built to close.
SILOED STATEOUT OF SYNClast 10:42last 10:39last 10:45≠≠Each radio keeps its own page of notes. No shared source of truth, so the picture drifts.
Keeping everyone on the same live picture ruled out a standard request-and-response page, which only updates when someone reloads. The system had to push state to every tablet the moment something changed, not wait for the next refresh.
02 — SOLUTION DESIGN
TABLETS, IN SYNCPERSISTENT CONNECTIONAJAX WRITE-THROUGHNode.jsWebSocketDATA STOREExisting stack, reporting
The design brought in new technology only where it earned its place. A persistent two-way connection over WebSockets, served by Node.js, carries the live broadcast: the moment a status changes on one tablet, the server pushes that change out to every other tablet at once. No one refreshes a page and nothing reloads; each screen stays current on its own. Everything else, the data storage and the reporting, stays on the existing stack, written through standard AJAX posts.
That split kept the persistence and the reporting on the platform the team already ran and maintained, while Node carried only the real-time layer. The interface itself is a web application running on the tablets, so a single codebase reaches every device over the network with nothing to install. The architecture isolates the one genuinely new capability instead of rebuilding the whole system around it.
03 — ROLE & LEADERSHIP
This came early in my career, and I led the build with mentorship from senior engineers along the way. I drove the architecture and built the web app the tablets ran on, from the client-side JavaScript to the Node server operations behind the real-time layer, which was new ground for all of us. I coordinated closely with an experienced backend developer who handled the data store and the AJAX persistence on our existing platform.
Before we committed to the full build, I stood up a small WebSocket prototype to test the one assumption everything depended on: whether our internal network could reliably hold persistent connections. Prototype, validate the riskiest assumption, then build. That sequence became a model for how the team approaches an unproven stack.
04 — TRADEOFFS
A persistent connection is not free. Unlike a page that simply reloads, a live connection has to manage reconnection and heartbeat behavior to stay reliable when the network blips. We took on that operational surface deliberately, because nothing short of a live connection could keep every tablet in sync the instant a status changed.
The simpler alternative, polling the server on a timer, would have been easier to operate but would have traded away the immediacy the whole system existed to provide. We accepted the added complexity of the real-time layer in exchange for state that is actually live.
05 — IMPACT
The system replaced the radio calls and the paper the floor team had relied on. What previously depended on whether someone heard the right radio call at the right moment now reached every tablet instantly. Status and reporting generate directly from the data store, where the attendance data lives, instead of being reconstructed from paper after the fact. The radio channels were freed for the operational communication they were meant for during Floor Sessions, rather than being tied up relaying status updates for someone to write down by hand.
A web application running live on tablets, fed by persistent real-time connections, was a new direction at the State Capitol at the time, and it proved its worth to stakeholders immediately. From the first Floor Session it ran, the floor team worked from one live picture instead of a paper record that was always a step behind. The system served reliably for nearly a decade of Floor Sessions.
It was also the first time our team had taken either Node.js or WebSockets into production. The approach that delivered it, prototype the riskiest assumption, prove it, then build, became the template the team uses to bring emerging technology into production without betting the whole system on it.
engineering
01 — CONTEXT
California State Assembly Member offices were fielding a high volume of meeting requests by phone and email. Each request was handled manually, tracked in inboxes and spreadsheets, with no structured way to capture, route, or report on them. As the volume grew, the offices needed a real system, not another workaround.
The constraint that shaped everything was a split audience. The people submitting requests are the public, so intake had to be reachable by anyone. The requests themselves describe who is meeting a legislator and when, which is exactly the information that cannot sit on a public surface. One system had to be open at the front and closed at the back.
02 — SOLUTION DESIGN
OPEN INTERNETSTATE INFRASTRUCTUREPUBLIC INTAKEWhere requests come inBOUNDARYSECURED DATAWhere requests liveDIST 1DIST 2DIST 3DIST 4SPEAKERPER-OFFICE ISOLATION
I designed the intake surface and the record of the request as two separate concerns rather than two tiers of one application. The public form's only job is to accept a submission and hand it off. It holds nothing, queries nothing, and has no view of any request once submitted, so the public surface never becomes a place worth attacking for the data.
From there, each request is routed into the office's own isolated portal, with permissions scoped so each office's schedulers see only their own data. The data model is designed around opt-in so each office can enable the system and configure its own intake rules independently.
Getting that boundary right first is what made the rest buildable. Notifications, per-office approval workflows, reporting, and the Speaker's office enhancements were all added afterward without revisiting the security model, because none of them changed where the data sat. A boundary decided late becomes a rewrite; decided first, it becomes the thing everything else assumes.
03 — ROLE & LEADERSHIP
I led the project and made the boundary decision first, before any feature work started, because every later choice depended on it. I designed the separation between the public intake surface and the request record, and architected a reusable form template with custom handlers so a new office could be brought on by configuration rather than by a new build. I led six developers across frontend and backend, and I ran the demos and pilot sessions with schedulers across the Capitol myself. Adoption was the real risk on this project: a scheduling system nobody trusts is a spreadsheet with extra steps, so the people who would live in it had to be convinced before it was built out.
Leading it was as much coordination as engineering. Each Member office had its own workflows, its own expectations, and its own pace. Getting offices to adopt meant showing schedulers, not IT staff, that the system worked for their day-to-day.
04 — TRADEOFFS
Building and running the system on internal infrastructure meant we carried the maintenance ourselves and onboarded offices one at a time rather than handing schedulers a self-service product they could turn on themselves.
We accepted that ongoing cost and the slower rollout because keeping the data secured inside the state boundary was the requirement everything else depended on. The tradeoff was ownership: full control over data residency in exchange for full responsibility over operations.
05 — IMPACT
The system gave each office a single place to receive, organize, and act on meeting requests. Over its lifetime, the platform has processed thousands of requests across participating offices.
The pilot launched with 5 Member offices and, over five years, reached roughly 60 active offices, with the request data secured on internal infrastructure throughout.
The system proved solid enough that I was later assigned to lead the development of the enhanced version for the Speaker of the Assembly's office, the highest-volume office in the California State Assembly, with added capabilities: batch processing, reviewer-based approval workflows, and advanced search.
The platform continues to evolve for Assembly offices today.
Before generative AI dominated the industry, Business Intelligence was the corporate focus. I started my career working directly with SAP products, earning certifications in data warehousing and ERP systems. Organizations used in-memory processing and real-time analytics to compress forecasting cycles and outpace competitors.
Every company wanted “data-driven” on their slide deck, much like they would later demand “blockchain” or “AI.” Vendors rebranded old products as intelligent, and executives wasted budgets chasing labels. The practitioners who actually shipped results were the ones who ignored the hype and connected the initiative to a stakeholder’s actual problem. That era taught me the lesson I still operate on today. The technology is rarely the hardest part of a project. Connecting the technology to the core value proposition is.
The same hype cycle played out with generative AI, but the underlying tooling eventually matured. Early developer extensions improved workflows with inline suggestions, but they felt like a ceiling. Anthropic’s Claude Code and the release of Opus 4.5 in late 2025 broke that assumption. Bringing planning, generation, and testing into a single CLI interface shifted the development paradigm entirely.
When anyone can bring an idea to a functional prototype in minutes, the industry floods with undisciplined code that collapses under real-world pressure. For seasoned software engineers, this moment clarified a major shift. The bottleneck moved from keystrokes to judgment. Architecture, systems thinking, and domain knowledge became significantly more valuable. The new tooling did not close the gap between disciplined and undisciplined practitioners. It widened it.
To build reliable systems at this speed, the practice had to evolve from prompt engineering to context engineering. Prompt engineering focuses on crafting the right question. Context engineering architects the environment. It defines what information the model sees, what persists across interactions, and how outputs flow back into the pipeline.
I encode project strategy directly into instruction files where the agents can actually use them. The Product Requirements Document serves as an executable contract between me and my agents. If I cannot write a test for a requirement, the requirement is too vague. AI agents run parallel workstreams with test-driven development acting as the quality gate.
The step most engineering teams skip is validation. I deploy pre-programmed inspection agents to run visual and functional checks against the original success criteria. They catch the regressions and scope drift that human review often misses. Scaling this orchestration across multiple independent loops required a dedicated layer, which became the foundation of my Banh Mi Ops framework.
Test-driven development, specification-driven design, and multi-agent frameworks all exist independently across the industry. The integration is what matters.
Most organizational value leaks at the handoffs. A strategist defines what a system should accomplish, and an engineer builds it. Somewhere between those two points, intent gets translated, compressed, and approximated. The final product often answers a slightly different question than the one originally asked.
Owning the entire pipeline from strategy through validation removes the translation step. The person who sat with the stakeholder and understands why a constraint exists is the same person running the agents that enforce it. When a requirement turns out to be impossible, it is discovered by someone who can renegotiate the scope rather than someone who has to escalate an issue. Reading business constraints and writing the code to solve them are usually treated as two separate jobs. I have found far more value in treating them as one.
Within the California Legislature, hundreds of applications and websites need the same core reference data: who the members are, the districts they represent, the committees they sit on, the offices and caucuses they belong to, and the deadlines and recesses that govern the calendar. For years each application kept its own hand-maintained copy, with no shared source to keep them aligned. For routine updates, that was workable.
The high-stakes moments were different: an election, the start of a new legislative session, a swearing-in. The data had to change everywhere at once, officially, and the content team updated the same member, the same assignment, the same district in application after application. The copies drifted: one record could read one way on one application and another way on the next. Giving each application its own store had been a reasonable trade when deadlines demanded it, but at scale, maintaining all those copies stopped being sustainable. The data needed one source.
SILOED COPIESOUT OF SYNCAPP 1APP 2APP 3APP 4≠≠≠Every application keeps its own copy. The copies drift.
02 — SOLUTION DESIGN
One service holds the reference data. Every application reads from it.
ONE SOURCESOURCE OF TRUTHone model, every entityAPP 1APP 2APP 3APP 4One source. Every application reads from it.
The schema has to outlast every other decision in the system. Members change. Committees re-form. New legislative sessions reset everything. I built one model that holds all of it.
It rests on two axes the institution runs on. Time, scoped through the legislative session each record belongs to. Structure, the relationships that connect members to the districts they represent, the committees they sit on, and the offices they hold. With those two in place, almost every question an application asks becomes a clean walk through the data.
Applications read the data in different ways. Some want a clean shaped read. Some want it computed, searched, or paginated. A few need raw access. I built the delivery layer to match those needs directly, so no application has to bend its shape around the wrong interface.
ON SAVEREGISTERED APPLICATIONSEDITORsaves a changeCENTRAL DATA SERVICEemits a change eventAPPLICATIONrefetches, stays freshAPPLICATIONrefetches, stays freshAPPLICATIONrefetches, stays freshUpdates sync on demand. No polling, no stale window.
Updates propagate on demand. When an editor saves a change, the service tells every application reading from it to fetch the new record. There is no polling interval. There is no window in which a record is current here and stale there. The one exception is a cache still serving an old record, and a cache can be cleared.
03 — ROLE & LEADERSHIP
What I set out to do was bring this service online at a level the institution could run on for the long term, legislative session after legislative session.
A developer on the team had started a smaller version of this idea for one application, and it showed the approach could work. I built on that work and turned it into the central data service for the team and the applications that read from it.
I designed the schema. I designed the API and wrote its specifications. I built the on-demand sync that pushes a change out to every application reading from the service the moment it is saved.
The longer game was making the service something the institution could run on its own. The web content team had been updating this data across the institution's sites by hand. The service gives them a single place to make those updates instead, with no engineer in the loop for routine work. The data stays accurate because the people who know it best are the ones keeping it that way.
04 — TRADEOFFS
Centralizing the data made one service responsible for what every application reads. If a record is wrong here, it is wrong everywhere. I accepted that on purpose. The alternative was what we already had: the same record wrong in different ways from one site to the next. Uptime concentrates the same way, though the cached copy each application already holds keeps it serving the last record through a brief outage rather than going down with the service.
The delivery layer gives applications more than one way to read the data. A team building a new application has to choose which pattern matches their needs and learn its conventions. I could have forced every application through the same shape, but the flexibility is what let each application adopt the service, reading the data in the way that suited it.
05 — IMPACT
More than twenty applications read from the service: internal tools, public rosters, seating charts, gallery slideshows, deadline calendars, mapping tools, and floor session apps. The same record, managed in one place, reaches all of them.
ONE SOURCEMANY APPLICATIONSCENTRAL DATA SERVICEMembers · DistrictsCommittees · DeadlinesPublic websiteInternal serviceCalendar / deadlinesMobile + web appMap BoundariesRoster / leadershipFloor appOne shaped source. Many different jobs.
The shift from one legislative session to the next used to be the longest lift of the year. The service makes it a routine operation now. When an election turns over a roster or a member is sworn in, the content team makes the change once and every application that reads from the service shows it.
The model outlived its first dataset. Other engineers extended the same approach to new datasets, built their own clients against the service, and shaped their own workflows on top of it. Some hardened the service further for historical records. The interface became something teams could build on rather than work around.
What I set out to do is what the service does today. The data is current. The content team keeps it accurate. The applications stay in sync. The institution runs on it legislative session after legislative session, and that is the bar I built it to meet.
An MBA is typically how an engineer announces they are leaving the codebase behind to pursue a management-related career. My experience did the opposite. I finished my degree and went deeper into building software.
People often ask why a software engineer pursues a business degree. I wanted to force my own growth by stepping into rooms where my technical background offered zero leverage.
Engineering teams reward the person who can produce the most efficient technical solution. A cohort of professionals from finance, healthcare, and the military operates on entirely different metrics. To succeed there, you have to raise a point and defend a position with stakeholders who do not share your vocabulary. The code cannot defend you. I needed to learn how to build consensus and drive decisions relying entirely on the strength of the business case.
This approach was established early on. My undergraduate degree was in management information systems at Sacramento State, a program built on the premise that business drives technology. It was actually there, while building my first tax calculator, that I discovered my passion for software development. The MBA took that foundational principle and turned it into an operational discipline.
Engineering trains you to find the objective answer. The test passes or fails. The profiler names the slow function. Arguments have clear, mathematical exits. Business realities lack that comfort. Throughout the program, I evaluated dozens of case studies, diagnosing organizational failures and analyzing strategic pivots within active companies. Analyzing these scenarios alongside professionals from diverse sectors exposed something I had not expected. Technical accuracy buys you nothing if you cannot connect it to the operational metrics those stakeholders actually care about. You have to work through situations where incomplete information is the default state and nobody agrees on the underlying facts.
That process shifts how an engineer thinks. The natural engineering reflex is to build. We see a challenge, we build a system, and we ship a product. That exact instinct frequently leads to severe overengineering, creating complex architecture for problems that require process changes rather than code. Business training forces a complete detachment from the technical system to focus exclusively on the core problem. It creates the discipline to look at a project scope and recognize that the most effective technical decision is often solving the issue through operational refinement rather than writing another line of code.
The program provided an immediate environment to apply this discipline. When my team was tasked with modernizing the Sacramento Entrepreneurship Academy (SEA), an institution that has developed regional founders for decades, the work began at the technical level. I wrote the code to revamp their program timelines for visual clarity and enhanced access to their digital portals.
Studying the management of innovation taught me that a modernized interface has limited impact if the organization itself remains siloed. The deeper infrastructure challenge was ecosystem alignment. Alongside the technical rollout, we initiated a strategic partnership between SEA and the Carlsen Center for Innovation and Entrepreneurship. We drew on the existing innovation hub in the Sacramento region to build a unified environment for future entrepreneurs. We aligned the operational realities of two distinct institutions to build a stronger local ecosystem. The project proved the exact lesson the case studies were teaching in the abstract. The most valuable architecture you can design is an alignment between people, and the code simply exists to facilitate it.
In business school, “competitive advantage” is often used to describe a unique capability that allows an organization to outperform its rivals. In government and large-scale public infrastructure, that advantage is found in individuals who can bridge the gap between strategic intent and technical execution. A pure administrator can identify organizational bottlenecks but lacks the vocabulary to evaluate the technical debt, infrastructure limits, or execution reality required to clear them. A pure engineer can build a computationally flawless system that solves the wrong operational problem, failing entirely to integrate the new technology into the existing business model.
The disconnect between those two groups is where large-scale initiatives stall. Working within the state’s technology infrastructure requires understanding how to introduce modernization while balancing the massive, ongoing core operations of government. It is the capacity to walk into a room, diagnose a strategic organizational failure, align cross-functional stakeholders on a path forward, and then sit down and architect the precise solution required to fix it.
When I was selected to speak as the commencement speaker for 1,200 graduating business students, the moment marked a quiet shift. The College of Business was being represented by a software engineer. The degree did not pull me away from my technical roots. It gave me the framework to guarantee I am architecting the right systems to shape the future of California.