Juan Pablo Gallegos Work Bulkify

Bulkify

Modeling checkout as an intention, not a transaction.

Backend Engineer · Freelance 2025–26 Nest.js DDD Hexagonal Mercado Pago
Role
Backend Engineer
Duration
5 months
Stack
Nest.js · TypeScript · PostgreSQL
Context
B2B machinery marketplace
Proof summary

What changed before the long read

The case is about giving a high-ticket marketplace a checkout model that matched how buyers actually commit.

Domain model
Checkout became an intention to purchase, not a premature order.
Integrity
Deposits, balances, and fulfillment moved through explicit states.
Handoff
The bounded context shipped with transition tests and an ADR.
01

Context

Bulkify is a B2B platform for listing and purchasing high-value industrial machinery — tractors, excavators, and industrial presses sold by regional dealers across Argentina. The platform handles the full transaction lifecycle: listing, discovery, negotiation, checkout, and delivery confirmation.

The backend was already structured around Hexagonal Architecture and Domain-Driven Design. Bounded contexts, aggregates, domain events, and ports/adapters were all established practice when I joined as a freelance backend engineer.

02

Problem

The existing checkout assumed a single atomic payment: a buyer pays the full amount and receives an order confirmation. This model broke for the platform's core use case.

Industrial equipment buyers typically place a deposit to reserve a machine weeks before taking delivery — with the remaining balance due on handoff. The old checkout had no concept of partial commitment, staged payment, or deferred confirmation. Operationally, this meant sales teams were managing payment stages in spreadsheets outside the system.

03

Constraints

The architecture was already committed to DDD. New work had to fit cleanly into bounded contexts, aggregate design, and domain events — no procedural shortcuts or direct DB calls across context boundaries.

Mercado Pago was the payment provider. Its webhook model distinguishes deposit pre-authorizations from captured payments, and its API surface changes with some regularity. The existing Order bounded context could not be broken — Checkout had to be entirely additive.

The engagement was five months with no guaranteed renewal, which put real pressure on handoff quality: the documentation had to be enough for another engineer to own Checkout confidently.

04

Role

I owned the entire Checkout bounded context end to end: domain modeling, application services, REST API contract, Mercado Pago integration, and domain event bus wiring to downstream contexts — Inventory (to hold stock on deposit) and Notifications (to trigger buyer confirmation emails).

I also wrote the integration test suite for the checkout state machine, covering every valid state transition and every attempt to reach an invalid one.

05

System shape

Checkout is modeled as an "intention to purchase" aggregate — a record of a buyer's commitment, entirely separate from the Order that governs delivery and logistics. A Checkout transitions through five explicit states:

Initiated
Deposit Paid
Balance Pending
Fully Paid
Fulfilled

Each transition is triggered by a domain event, not a direct mutation. The aggregate enforces its own invariants: a deposit cannot be refunded once the balance is locked; a checkout cannot be fulfilled until payment is complete. The Mercado Pago surface is wrapped entirely behind a payment port/adapter — the domain never imports the payment SDK directly.

Checkout contract
Bounded context seam
Aggregate
Separate commitment from logistics. Checkout owns payment intent; Order owns delivery and fulfillment.
Events
Publish facts, not service calls. Deposit and full-payment events let Inventory and Notifications react independently.
Provider
Contain Mercado Pago volatility. Webhooks and payment API changes stop at the adapter boundary.
06

Key decisions

Decision 01
Separate Checkout from Order

Checkout and Order have different lifecycles and different invariants. Checkout is about payment commitment; Order is about fulfillment and logistics. Collapsing them forces impossible states — a fulfilled order with an unpaid deposit, or a paid checkout with no delivery record.

Decision 02
Explicit state machine over boolean flags

Boolean flags like isPaid, hasDeposit, isComplete create 2ⁿ possible states, most of them invalid. An explicit state machine makes illegal transitions impossible by construction and makes every valid path testable in isolation.

Decision 03
Domain events over direct service calls

CheckoutDepositReceived and CheckoutFullyPaid are published to an internal event bus. Inventory and Notifications subscribe independently. Checkout stays unaware of its consumers, making it independently testable and deployable.

07

Trade-offs

Gained

  • Illegal payment states are impossible by construction
  • Checkout, Order, and Payment are independently deployable
  • Every state transition has a focused, isolated unit test
  • Mercado Pago API changes absorbed in one adapter file

Cost

  • Higher upfront complexity: two aggregates instead of one
  • More infrastructure: event bus, webhook handler, adapter layer
  • Higher onboarding cost for engineers unfamiliar with DDD
08

Outcome

The deposit payment system shipped and is live in production. Buyers place deposits to reserve machines, receive automated confirmation emails, and settle the balance before delivery. No payment integrity incidents since launch. The Checkout bounded context was handed off with full test coverage and an architecture decision record.

09

Lessons

1
The key insight wasn't technical — it was linguistic. Once the team agreed that "a buyer expresses an intention to purchase, not an immediate transaction," the state machine, the API contract, and the event names all followed naturally. DDD vocabulary is a design tool first.
2
Wrapping a payment provider behind an adapter is not over-engineering. Mercado Pago changed its webhook signature once during the engagement. The change was absorbed in a single file, leaving the domain untouched.
3
In a time-limited freelance engagement, the most important deliverable is the architecture decision record — not just the code. The code ships once; the record answers questions for months after you leave.