~/work/soros/
SOROS
A marketplace that grew from an early Next.js storefront into Go and AWS systems for checkout, fulfillment, integrations, and operator-facing work.

- Role
- Contract Software Engineer · New Studio
- Period
- Dec 2024 – Nov 2025
- System
- marketplace systems
The first real failure was not a UI bug
SOROS started feeling like a real product when the frontend stopped being the hardest part.
A secret lookup could succeed while the database connection still failed. The page in front of me looked fine, the API shape looked reasonable, and none of that mattered because a request still had to cross a private network, reach the correct database, return through the right response path, and survive browser rules around cookies and CORS.
The repository records the messy version of that week: network and security-group corrections, a proxy experiment, cleanup commits, and a database decision that changed from a document-oriented setup to PostgreSQL and GORM. The infrastructure was decomposed, reconsidered, and simplified while the product was already moving. There was no architecture diagram that had predicted the right answer.
That episode became the pattern for roughly a year of work. A visible interaction was only the front edge of a deeper state transition. When something failed, I had to follow it across the browser, Go handler, persistence model, cloud boundary, and operator workflow instead of polishing the nearest layer and calling the problem solved.
Before the backend, there was a product language
The cloud failure makes more sense after rewinding to the beginning.
Kyrylo Orlov created the complete web design in Figma, and Jonathan Lin created the brand identity and assets. They gave me the visual system I brought to life in Next.js before I expanded the product into the backend, infrastructure, and operational work described here.
The first commit was a blank Next.js application. On the same day, I started building the small pieces a marketplace would need: buttons with real variants, inputs, selection controls, icons, and form behavior. Over the next several weeks those primitives turned into product cards, seller cards, filters, search, navigation, dashboards, messages, media fields, charts, modals, and the first product and storefront surfaces.
The important decision was not the framework. It was building shared interaction rules before every page became its own exception. A select that did not open, a sticky header with the wrong scroll calculation, a search field that disappeared at the wrong width, or a gesture that worked with a mouse but not a swipe all forced small revisions. Those corrections made the product language concrete.
At that stage I was thinking mostly like a frontend and product engineer: what should the buyer see, what should a seller be able to change, and which interaction should feel immediate? The later systems work grew out of those questions. The backend was not a separate technical exercise. It was the place where the promises made by those interfaces had to become true.
The prototype met infrastructure
By late November, a marketplace page could no longer pretend it was the whole product.
The Go backend began with a small serverless shape and a shared response convention. It quickly expanded into accounts, addresses, stores, orders, messages, media, and administrative routes. Authentication moved from wiring into real login, signup, verification, session, and profile flows. That is where simple assumptions broke: a correct handler could still fail because the browser rejected a cookie, an origin was wrong, a response carried the wrong error semantics, or an environment pointed to the wrong resource.
The database pivot mattered because it was an early lesson in revising an implementation instead of defending it. The initial Mongo and DocumentDB direction was replaced with SQL models and PostgreSQL when the operational and modeling fit became clearer. The surrounding cloud templates were split into smaller network, database, and API concerns, then simplified again as deployment behavior became easier to observe.
Testing became part of that loop in December. Handler tests, repeatable request fixtures, a test runner, and CI made account and media behavior inspectable without clicking through every path. Upload and moderation work crossed React hooks, Go handlers, object storage, and user profile state. Domain, HTTPS, and environment-aware CORS fixes completed the picture: shipping the interface meant owning everything required for a user action to make the round trip.
Teaching Go the marketplace
Go stopped being the language behind the API and became the place where the product was defined.
The backend grew around explicit handlers, request models, GORM records, and shared response helpers. A handler had to decode input, validate it, recover the authenticated actor, load the relevant records, enforce a state transition, persist the result, and return a response the frontend could actually use. That sounds ordinary until the same order can be touched by a buyer, seller, payment provider, carrier, scheduled worker, and administrator.
Early code often kept those steps close together because the behavior was still being discovered. As the system grew, repeated checks and transformations became seams: address normalization, ownership checks, product availability, order totals, payout eligibility, provider identifiers, and error mapping. The goal was not to manufacture layers. It was to keep one handler from quietly inventing a different definition of an order than the next handler.
GORM made the relational structure visible, but it did not make the domain decisions automatic. Associations, preload behavior, nullable provider fields, lifecycle timestamps, and enum-like status values all affected what a request meant. A missing row could be a normal empty result, a deleted resource, or evidence that two systems had drifted. I learned to make those distinctions explicit in Go instead of relying on whatever error happened to emerge from the database call.
The same discipline applied at the boundary. Shared response helpers kept status codes and JSON shapes consistent. Middleware attached authentication and cross-cutting behavior without duplicating it in every route. Provider adapters translated external payloads into application types so checkout and fulfillment code did not become a pile of vendor-specific branches. The backend became easier to change when its vocabulary matched the marketplace rather than the infrastructure underneath it.
This was also where tests became useful as design pressure. Handler tests exposed functions that depended on ambient configuration, generated identifiers, or a live provider. Moving those dependencies behind helpers and adapters made the code easier to exercise and made production failures easier to classify. The test was not just proof that a route returned 200. It was a way to see whether the Go code had a boundary clear enough to control.
Money and fulfillment changed where authority belonged
A marketplace happy path became much harder once an order crossed payment and carrier boundaries.
Checkout had to coordinate products, discounts, shipping choices, payment confirmation, order state, refunds, labels, tracking, and staff actions. Multi-chain payment support added another source of external truth. A transaction could exist without matching the order a user thought they had placed, and a cancellation or refund could not be reduced to changing a label in the interface.
The decisive revision was moving final pricing and payment authority away from the browser. Instead of allowing the client to calculate and declare the final order, the Go backend produced a checkout quote from the current products, discounts, shipping inputs, and payment rules. Confirmation referred back to that server-side quote. The frontend still explained the result and collected the next input, but it no longer decided what the buyer owed.
That change forced the Go code to become more precise. Totals had to be composed from values with different owners and lifetimes. A product price came from the catalog, shipping came from a carrier or policy decision, discounts had eligibility rules, and the payment provider reported its own state. The quote and confirmation handlers became an authority boundary: they reloaded what could change, rejected stale assumptions, and persisted the exact state used to create the order.
Money was a coordination problem
Accepting a transaction was only the beginning of the payment system.
The backend needed to recognize a transaction across supported networks, associate it with the intended order, normalize provider-specific fields, and retain enough evidence to explain the decision later. Conversion rates and fee estimates could not be fetched blindly on every request, so cached values, fallback behavior, and freshness became part of the payment path. Amount handling had to avoid letting display formatting leak into comparison logic.
Payouts added a second lifecycle. A successful customer payment did not mean every downstream conversion or transfer would finish in the same request. I kept that work off the critical checkout path and modeled it as durable state: pending work, successful completion, retryable failure, terminal failure, and manual intervention. Scheduled Go workers could revisit pending records, but the database remained the history of what had already been attempted.
That distinction changed how errors were written. A provider timeout was not the same as a rejected transaction. A missing balance was not the same as malformed configuration. A retry that could safely repeat needed different handling from an operation that might already have taken effect. Classifying those outcomes in Go made retries safer and gave operator-facing tools something more useful than a raw error string.
The administrative path mattered as much as the scheduler. Operators needed to see conversion history, inspect the latest result, and trigger a controlled retry without reconstructing the request from logs. That requirement fed back into the data model and adapters: store provider references, preserve timestamps and reason codes, and make the same core operation callable from both automated and manual entrypoints.
Fulfillment became a state machine
Shipping was not one API call. It was policy distributed across quoting, persistence, disclosure, labels, tracking, and payouts.
The first implementation could ask a carrier for rates and show choices. The harder version had to handle orders whose value or contents changed the rules. Additional handling or insurance could affect the buyer total, seller proceeds, carrier request, stored shipment, and the explanation shown before purchase. A rule implemented in only one of those places was not a rule. It was a future reconciliation bug.
I moved that behavior into shared Go calculations and kept monetary allocation in integer cents where the domain allowed it. Quote handlers returned enough structure for the frontend to explain the result without recomputing it. Confirmation revalidated the selected option. Order creation persisted the values used, and seller and staff views read from that state rather than inventing their own totals.
The lifecycle continued after purchase: create or retry a label, retain the carrier reference, update tracking, handle cancellation, and expose enough status for staff to intervene. Those transitions were implemented as guarded operations rather than arbitrary status writes. A retry should be safe when the first request failed before the provider accepted it, and cautious when acceptance was uncertain.
This work made fulfillment a good test of architecture. React components needed clear disclosures, Go handlers needed authoritative calculations, GORM models needed durable state, adapters needed provider-specific translation, and tests needed to cover both the straightforward route and the awkward partial failures. The feature only worked when those pieces agreed.
Integrations taught me to reconcile, not import
External systems rarely behave like a one-time data source.
Catalog integrations began with OAuth, token handling, product mapping, and pagination, but the harder questions arrived immediately afterward. Which identifier survives a round trip? What happens when a seller edits an item inside SOROS after an external import? Can a blank field from a provider erase a useful local value? What does disconnect mean if webhooks are still registered?
The Go integration code moved toward reconciliation: persist external identifiers, renew tokens deliberately, pace paginated requests, preserve intentional local edits, and treat provider defaults as inputs rather than truth. A product could have an internal identity, an external product identity, and one or more variant or inventory identities. Flattening those into one string made the first import easier and every later synchronization harder.
Webhook handlers introduced a different boundary. Signature verification sometimes depended on the raw request body, while normal application code wanted decoded structs. Event delivery could repeat, arrive out of order, or reference an object that had changed again before processing. The handler therefore needed to verify before decoding, retain the external event identity, and make downstream inventory or order adjustments idempotent.
Provider APIs also changed the shape of the client code. One integration used paginated REST-style resources, another used GraphQL queries and mutations with nested edges and provider-specific error collections. Adapters normalized those differences into application operations, but they did not hide useful failure detail. The operator still needed to know whether authentication expired, a mapping was missing, or the provider rejected a specific item.
Synchronization is a continuing relationship between two authorities. The integration must remember what it last saw, what it changed, and what the user changed locally. That is much more programming than an import button suggests, and it became one of the strongest examples of why state history matters.
Changing the runtime without rewriting the product
The API migration was a cutover problem, not a framework announcement.
By the time the backend had many HTTP and scheduled entrypoints, deployment and routing behavior had become too coupled to individual Lambda functions. Path-substring dispatch was brittle, API documentation could drift, and each runtime change repeated infrastructure work. Rewriting the domain logic would have replaced one risk with a larger one.
I introduced an explicit Go route registry and an HTTP adapter around the existing handlers. The registry described method, path, handler, and relevant middleware in one place. That same definition could drive the original entrypoint behavior, a consolidated Go server, and a generated OpenAPI contract. Scheduled work stayed separate from HTTP work instead of being forced through the new server.
The adapter was the seam that made the migration practical. It translated the server request into the shape existing handlers expected, carried context and authentication through middleware, and converted the handler result back into an HTTP response. CORS, error mapping, logging, and panic recovery could be applied consistently. Domain code did not need to know whether it had been reached through a function invocation or a containerized server.
Container packaging was the easy milestone to celebrate and the wrong place to stop. The real cutover included health checks, load-balancer behavior, certificate and hostname collisions, environment target mismatches, database-proxy behavior, log retention, and a user-facing maintenance path. A server that compiled and listened on a port was not yet a production service.
I wrote the runbook around known failure modes because a migration is complete only when someone can tell whether traffic is healthy, understand what failed, and recover without guessing. That made rollback expectations, old and new routing, and operator-visible signals part of the implementation rather than deployment folklore.
Reliability was part of the feature
The most useful reliability work made failures explainable before it tried to make them impossible.
The Go test suite grew around handlers, helpers, and the transitions most likely to drift: authentication responses, media behavior, order calculations, provider mapping, payout state, and shipping rules. Request fixtures and mockable adapters kept those tests focused. CI made the result repeatable instead of relying on a local sequence that only I knew how to run.
Logging became more structured as asynchronous work and integrations grew. A useful record needed the operation, internal entity, provider reference when safe to store, attempt state, and enough context to distinguish one failure class from another. Raw logs were still necessary, but durable job and domain status meant an operator did not need to grep a deployment to answer every question.
Administrative interfaces were therefore engineering surfaces, not afterthoughts. They exposed reconciliation results, payment and payout history, shipping status, moderation state, and controlled recovery actions. Building them often revealed missing data in the backend model. If the UI could not explain why an action was blocked or what a worker had done, the Go service probably had not recorded enough.
I also learned where automation should stop. Some errors were safe to retry with backoff. Some were terminal until configuration or data changed. Some needed a person because repeating them could duplicate an external effect. Reliability came from encoding those differences and giving the next person a clear intervention path, not from surrounding every call with the same retry loop.
What I carried forward
SOROS changed the way I decide where software behavior belongs.
I began the project by making screens and interactions coherent. I left it thinking in boundaries: the browser can explain and collect, but the Go server must authorize; a provider can notify, but the application must reconcile; a scheduler can retry, but an operator still needs visibility and control.
It also made infrastructure feel less like a deployment layer and more like part of the product. A database choice, cookie policy, health check, route seam, or rollback path changes what users and operators can trust. The strongest architecture was rarely the most elaborate one. It was the one that made responsibility explicit enough to test, observe, and revise.
That is the thread I bring to newer work in storage, filesystems, performance, and lower-level systems. The domain changes, but the habit is the same: follow the state across boundaries, put authority where it can be enforced, and leave enough evidence for the next failure to be diagnosable.
- Use Go handlers and shared domain helpers to keep authorization, validation, and persistence rules consistent.
- Put financial and fulfillment policy on the server; let the client explain and collect.
- Design integrations around identifiers, idempotency, token lifecycle, webhooks, and recovery from the first provider call.
- Pair scheduled automation with durable status, classified retries, and a human intervention path.
- Treat runtime migrations as behavior-preserving cutovers with health, logs, maintenance, and rollback.