diff --git a/README.md b/README.md index 3c12a44..cd67b76 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,13 @@ I build developer tools that make AI agents smarter. > I write about AI-driven development, MCP integrations, and developer tooling at **[codestz.dev](https://codestz.dev)** +- [Stop Loading 100K Tokens Just to Call a Tool: Why I Built MCPX](https://codestz.dev/experiments/mcpx-mcp-gateway) - [The AI Convergence: Why Every Coding Tool Lands on the Same Two Defaults](https://codestz.dev/experiments/why-ai-defaults-to-typescript) - [MCP Is a Crutch: Why CLI Tools Are the Future of AI Agent Tooling](https://codestz.dev/experiments/mcpx-cli-over-mcp) - [SPARC: The Methodology That Turns Vibe Coding Into Actual Engineering](https://codestz.dev/experiments/sparc-methodology-ai-development) - [Spec-Driven Development: Why Writing Specs Before Code Is the Next Paradigm Shift](https://codestz.dev/experiments/spec-driven-development-tessl) - [RTK: The Rust Binary That Slashed My Claude Code Token Usage by 70%](https://codestz.dev/experiments/rtk-rust-token-killer) - [From Vibe Coding to Symbolic Reasoning: How Serena MCP Gives AI Agents X-Ray Vision](https://codestz.dev/experiments/serena-mcp-architectural-mastery) -- [Surgical Code Editing: The 10x Token Efficiency Pattern](https://codestz.dev/experiments/surgical-code-editing) --- diff --git a/mdx-components.tsx b/mdx-components.tsx index 9dbdedb..ea2e126 100644 --- a/mdx-components.tsx +++ b/mdx-components.tsx @@ -10,6 +10,9 @@ import { Callout, ProcessFlow, StatBlock, + ScopeBlock, + DecisionLog, + MilestoneCard, } from '@/components/mdx'; import { Mermaid } from '@/components/mdx/Mermaid'; @@ -32,6 +35,9 @@ export const mdxComponents: MDXComponents = { Callout, ProcessFlow, StatBlock, + ScopeBlock, + DecisionLog, + MilestoneCard, // Headings h1: ({ children }) => (

diff --git a/package.json b/package.json index 5c79d05..72ea568 100644 --- a/package.json +++ b/package.json @@ -59,5 +59,11 @@ "shiki": "^3.22.0", "tailwindcss": "^4", "typescript": "^5" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sharp", + "unrs-resolver" + ] } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b784396..3e1fe7e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,6 @@ packages: - '.' -ignoredBuiltDependencies: - - sharp - - unrs-resolver +allowBuilds: + sharp: true + unrs-resolver: true diff --git a/public/images/blog/stop-vibe-coding-start-vibe-engineering.png b/public/images/blog/stop-vibe-coding-start-vibe-engineering.png new file mode 100644 index 0000000..02cf49a Binary files /dev/null and b/public/images/blog/stop-vibe-coding-start-vibe-engineering.png differ diff --git a/src/components/mdx/DecisionLog/DecisionLog.tsx b/src/components/mdx/DecisionLog/DecisionLog.tsx new file mode 100644 index 0000000..a7996eb --- /dev/null +++ b/src/components/mdx/DecisionLog/DecisionLog.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { Check, X, Clock, Sparkles } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { DecisionLogProps, DecisionVerdict } from './DecisionLog.types'; + +const verdictConfig: Record< + DecisionVerdict, + { label: string; Icon: typeof Check; chipClass: string; barClass: string } +> = { + rejected: { + label: 'Rejected', + Icon: X, + chipClass: 'bg-red-500 text-white border-red-700', + barClass: 'border-l-red-500', + }, + accepted: { + label: 'Accepted', + Icon: Check, + chipClass: 'bg-green-500 text-white border-green-700', + barClass: 'border-l-green-500', + }, + deferred: { + label: 'Deferred', + Icon: Clock, + chipClass: 'bg-amber-500 text-black border-amber-700', + barClass: 'border-l-amber-500', + }, +}; + +/** + * DecisionLog Component - AI Proposal / Verdict / Reasoning Log + * Neo-Brutalist visual for capturing the "moments I said no/yes/later" pattern. + * Reusable on any AI-workflow post. + */ +export function DecisionLog({ title = 'Decision Log', decisions, className }: DecisionLogProps) { + return ( +
+
+ + {title} + +
+ +
+ {decisions.map((d, idx) => { + const v = verdictConfig[d.verdict]; + const Icon = v.Icon; + return ( +
+
+ + + + Model: + + {d.proposal} + + + + {v.label} + +
+ +
+ + Why: + + {d.reasoning} +
+
+ ); + })} +
+
+ ); +} diff --git a/src/components/mdx/DecisionLog/DecisionLog.types.ts b/src/components/mdx/DecisionLog/DecisionLog.types.ts new file mode 100644 index 0000000..465e47f --- /dev/null +++ b/src/components/mdx/DecisionLog/DecisionLog.types.ts @@ -0,0 +1,13 @@ +export type DecisionVerdict = 'rejected' | 'accepted' | 'deferred'; + +export interface Decision { + proposal: string; + verdict: DecisionVerdict; + reasoning: string; +} + +export interface DecisionLogProps { + title?: string; + decisions: Decision[]; + className?: string; +} diff --git a/src/components/mdx/DecisionLog/index.ts b/src/components/mdx/DecisionLog/index.ts new file mode 100644 index 0000000..bf14593 --- /dev/null +++ b/src/components/mdx/DecisionLog/index.ts @@ -0,0 +1,2 @@ +export { DecisionLog } from './DecisionLog'; +export type { DecisionLogProps, Decision, DecisionVerdict } from './DecisionLog.types'; diff --git a/src/components/mdx/MilestoneCard/MilestoneCard.tsx b/src/components/mdx/MilestoneCard/MilestoneCard.tsx new file mode 100644 index 0000000..8bc3619 --- /dev/null +++ b/src/components/mdx/MilestoneCard/MilestoneCard.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { ArrowRight, Award, Calendar, Mic, Rocket, Sparkles, TrendingUp } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { MilestoneCardProps, MilestoneKind } from './MilestoneCard.types'; + +const kindConfig: Record = { + promotion: { label: 'Promotion', Icon: TrendingUp }, + award: { label: 'Award', Icon: Award }, + launch: { label: 'Launch', Icon: Rocket }, + talk: { label: 'Talk', Icon: Mic }, + milestone: { label: 'Milestone', Icon: Sparkles }, +}; + +/** + * MilestoneCard Component - Career milestone callout + * Neo-Brutalist visual for a single career event: promotion, award, launch, talk. + * Optional FROM → TO transition row makes it especially fit for promotions. + * Slim layout: header strip + title row + optional transition + optional highlights. + */ +export function MilestoneCard({ + title, + subtitle, + kind = 'milestone', + date, + from, + to, + highlights, + className, +}: MilestoneCardProps) { + const { label, Icon } = kindConfig[kind]; + + return ( +
+
+ + + {label} + + {date && ( + + + {date} + + )} +
+ +
+

+ {title} +

+ {subtitle && ( +

{subtitle}

+ )} + + {(from || to) && ( +
+ + {from ?? '—'} + + + {to ?? '—'} +
+ )} + + {highlights && highlights.length > 0 && ( +
    + {highlights.map((item, idx) => ( +
  • + + {item} +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/src/components/mdx/MilestoneCard/MilestoneCard.types.ts b/src/components/mdx/MilestoneCard/MilestoneCard.types.ts new file mode 100644 index 0000000..54219c2 --- /dev/null +++ b/src/components/mdx/MilestoneCard/MilestoneCard.types.ts @@ -0,0 +1,12 @@ +export type MilestoneKind = 'promotion' | 'award' | 'launch' | 'talk' | 'milestone'; + +export interface MilestoneCardProps { + title: string; + subtitle?: string; + kind?: MilestoneKind; + date?: string; + from?: string; + to?: string; + highlights?: string[]; + className?: string; +} diff --git a/src/components/mdx/MilestoneCard/index.ts b/src/components/mdx/MilestoneCard/index.ts new file mode 100644 index 0000000..09d5931 --- /dev/null +++ b/src/components/mdx/MilestoneCard/index.ts @@ -0,0 +1,2 @@ +export { MilestoneCard } from './MilestoneCard'; +export type { MilestoneCardProps, MilestoneKind } from './MilestoneCard.types'; diff --git a/src/components/mdx/ScopeBlock/ScopeBlock.tsx b/src/components/mdx/ScopeBlock/ScopeBlock.tsx new file mode 100644 index 0000000..3153b0e --- /dev/null +++ b/src/components/mdx/ScopeBlock/ScopeBlock.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { Check, X, Lock } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { ScopeBlockProps } from './ScopeBlock.types'; + +const columns = [ + { + key: 'building', + label: 'Building', + Icon: Check, + tone: 'text-green-700 dark:text-green-400', + barTone: 'border-l-green-500 bg-green-50/40 dark:bg-green-950/20', + }, + { + key: 'notBuilding', + label: 'Not Building', + Icon: X, + tone: 'text-red-700 dark:text-red-400', + barTone: 'border-l-red-500 bg-red-50/40 dark:bg-red-950/20', + }, + { + key: 'constraints', + label: 'Constraints', + Icon: Lock, + tone: 'text-amber-700 dark:text-amber-400', + barTone: 'border-l-amber-500 bg-amber-50/40 dark:bg-amber-950/20', + }, +] as const; + +/** + * ScopeBlock Component - Three-Column Scope Discipline Panel + * Neo-Brutalist visual for declaring what's in, what's out, and the constraints. + * Reusable on any planning/scoping post. + */ +export function ScopeBlock({ + title = 'Scope', + building, + notBuilding, + constraints, + className, +}: ScopeBlockProps) { + const data = { building, notBuilding, constraints }; + + return ( +
+
+ + {title} + +
+ +
+ {columns.map((col) => { + const items = data[col.key]; + const Icon = col.Icon; + return ( +
+
+ + + {col.label} + + {items.length} +
+ +
    + {items.map((item, idx) => ( +
  • + + {item} +
  • + ))} +
+
+ ); + })} +
+
+ ); +} diff --git a/src/components/mdx/ScopeBlock/ScopeBlock.types.ts b/src/components/mdx/ScopeBlock/ScopeBlock.types.ts new file mode 100644 index 0000000..0bc1021 --- /dev/null +++ b/src/components/mdx/ScopeBlock/ScopeBlock.types.ts @@ -0,0 +1,7 @@ +export interface ScopeBlockProps { + title?: string; + building: string[]; + notBuilding: string[]; + constraints: string[]; + className?: string; +} diff --git a/src/components/mdx/ScopeBlock/index.ts b/src/components/mdx/ScopeBlock/index.ts new file mode 100644 index 0000000..89eea54 --- /dev/null +++ b/src/components/mdx/ScopeBlock/index.ts @@ -0,0 +1,2 @@ +export { ScopeBlock } from './ScopeBlock'; +export type { ScopeBlockProps } from './ScopeBlock.types'; diff --git a/src/components/mdx/index.ts b/src/components/mdx/index.ts index f4d02bf..fa7ff07 100644 --- a/src/components/mdx/index.ts +++ b/src/components/mdx/index.ts @@ -20,3 +20,9 @@ export { ProcessFlow } from './ProcessFlow'; export type { ProcessFlowProps, ProcessStep } from './ProcessFlow'; export { StatBlock } from './StatBlock'; export type { StatBlockProps, Stat } from './StatBlock'; +export { ScopeBlock } from './ScopeBlock'; +export type { ScopeBlockProps } from './ScopeBlock'; +export { DecisionLog } from './DecisionLog'; +export type { DecisionLogProps, Decision, DecisionVerdict } from './DecisionLog'; +export { MilestoneCard } from './MilestoneCard'; +export type { MilestoneCardProps, MilestoneKind } from './MilestoneCard'; diff --git a/src/content/blog/stop-vibe-coding-start-vibe-engineering.mdx b/src/content/blog/stop-vibe-coding-start-vibe-engineering.mdx new file mode 100644 index 0000000..9ce2d75 --- /dev/null +++ b/src/content/blog/stop-vibe-coding-start-vibe-engineering.mdx @@ -0,0 +1,480 @@ +--- +title: 'Stop Vibe Coding. Start Vibe Engineering.' +description: 'I shipped Mintroot — a real Android finance app with 10 entities, 12 milestones, and a 4-layer matching engine — in 48 hours via a single Opus 4.7 1M-context conversation. The 1M context was not the trick. What I did before the first line of Kotlin was.' +publishedAt: '2026-05-11' +category: 'ai' +tags: + [ + 'vibe-engineering', + 'ai-workflow', + 'claude-opus', + 'long-context', + 'software-engineering', + 'agentic-development', + ] +featured: true +type: 'experience' +author: 'Esteban Estrada' +thumbnail: '/images/blog/stop-vibe-coding-start-vibe-engineering.png' +--- + +# Stop Vibe Coding. Start Vibe Engineering. + +I bank with Bancolombia. Their statement exports are XLSX files where the same workbook mixes US locale (`1,375,571.00`) and Colombian locale (`1.375.571,00`) for two different fields. Credit card statements come in three layouts depending on the network (Visa, Mastercard, Amex), and the multi-currency cards split into `PESOS` and `DOLARES` sheets that share a credit limit. Installment plans show up encoded as `cuotas X/Y` in a single cell. Reversals appear as ghost duplicates with matching authorization codes. Every existing personal finance app either ignores the format or asks for my banking credentials. + +So I built [Mintroot](https://github.com/Codestz/Mintroot). Native Android. Kotlin + Jetpack Compose. Encrypted local database. Gemini for classification with a user-supplied key. Forty-eight hours, end to end, one conversation with Opus 4.7 over a 1M-token context window. Signed APK on hour 47. + +That's the part that gets attention. It's not the story. + +**The story is what happened in the first three hours of that conversation, before a single line of Kotlin existed.** I named the problem. I scoped it. I wrote a list of what I would not build. I sketched ten domain entities. I rejected three features the model offered. By the time the first `BancolombiaSavingsParser` got generated, the architecture was already decided — not by the model, by me, with the model as a thinking partner. + +That gap — between "let the model decide" and "decide with the model" — is the difference between vibe coding and vibe engineering. And it is the entire reason this project shipped instead of becoming another half-working repo on my laptop. + + + +## Vibe Coding vs Vibe Engineering + +Let me define both before anyone reads their own definition into them. + +**Vibe coding** is prompt-and-pray. You open a chat, describe a thing, accept whatever lands, glue it together, hope it runs. The artifact is the goal. The model is the architect, the developer, and the QA. You are the typist. If it works, you ship. If it doesn't, you regenerate. + +**Vibe engineering** treats AI as leverage on the parts of engineering that scale: typing, recall, exploration, refactoring. The judgment — what to build, why, how it fits, what to cut — stays with you. The model writes the code. You own the problem. + + + +The trap is that vibe coding feels productive. Files appear. The terminal scrolls. Something compiles. Dopamine. But producing artifacts is not the same as solving a problem, and a repo full of generated code that doesn't ship is not engineering — it's just a tour of what the model felt like writing today. + +## The Four Moves + +Vibe engineering, the way I run it, is four moves in order. I do them with the model in the room, but I do them. Skip any of the first three and you are vibe coding with extra steps. + + + +### 1. Identify + +Before any code, I want one sentence: who hurts, and how. Not "an app for finances." A real, irritating, named pain. + +Mintroot's pain has a name and a shape. Bancolombia's XLSX exports come in three structural layouts that share almost nothing: + +- **Savings** is one sheet, header rows 2–12, movements rows 14+, date format `D/MM` with the year inferred by walking forward through the rows and detecting month wraparounds. Period crosses a calendar quarter. The same statement repeats `Información Cliente` / `Movimientos` header bands every ~50 rows because Excel doesn't paginate well. +- **Credit cards** are one or two sheets named `PESOS` and `DOLARES`. The dollar sheet can be empty but still indicates a multi-currency card. Authorization codes are 6-digit numeric for Visa/Amex but letter-prefixed alphanumeric for Mastercard, and the Mastercard prefix encodes the operation type (`R*` purchase, `C*` payment, `T*` installment). `Pago mínimo` uses US locale; `Pago total` uses Colombian locale; same workbook, same column. +- **Investment funds** use date format `YYYYMMDD` with no separators, and yield accrual is _invisible in the movement table_ — it compounds into the unit value and surfaces only in the Resumen block. + +That's a problem with a shape. I can describe done in one line: _import Bancolombia XLSX of any of those three shapes, classify the transactions, and answer questions about my money in plain Spanish._ If I cannot do that in one sentence, I do not start. + +> If you can't name the pain in one sentence, you don't have a project. You have a vibe. + +### 2. Planify + +Scope is a list of what you will not build. I wrote that list first, in hour two of the conversation, and the model never saw a "build the app" prompt until the list was locked. + + + +The "not building" column is what saved this project. Every time the model suggested a feature — and Opus is generous with suggestions — I checked it against that list. Multi-bank adapter abstraction? Not building. Cloud backup? Not building. Onboarding wizard? Not building. The list outranked the model every time. + +This is the move vibe coding skips. When you don't write down what you will not build, the model fills the silence with everything it can imagine, and you end up shipping nothing because you tried to ship everything. + +### 3. Structure + +Once the scope is locked, I draw the system. Not in detail — bones first. Where the layers are, who owns what data, where the boundaries are, what state machines govern which entities. + +```mermaid +graph TD + UI["Presentation Layer
Compose + Material 3 Expressive"] + Domain["Domain Layer
Use cases, matching, analytics"] + Data["Data Layer
Room + parsers + repositories"] + Infra["Infrastructure
Gemini client, WorkManager, FS"] + Store["Local Storage
SQLCipher DB + _archive/ files"] + + UI -->|StateFlow| Domain + Domain --> Data + Domain --> Infra + Data --> Store + Infra --> Store + + Files[XLSX statements] -.->|inbox / share sheet| Infra + User[User taps + chat] -.-> UI +``` + +The boring rules: `ui/` never imports `data/` directly. `domain/` is pure Kotlin, no Android imports. The Gemini client is one place, so the prompts are one place, so the taxonomy is one place. The parser is mechanical, the classifier is adaptive, and they meet at one interface: `ParsedStatement` → `List`. + +This is also where I locked the most decision-dense part of the project: the data model. Ten primary entities, all of them argued for in conversation before any of them got generated. + + + +I also wrote down a thesis for the AI half of the system, in hour three, that the entire chat then operated under: + + + The LLM is the cognitive engine. It does the thinking. The database is its memory. The user is the + editor of last resort. The cache means the LLM does not classify the same merchant 200 times — the + first time it sees `RAPPI COLOMBIA*DL` it produces a structured classification with reasoning, and + from then on the cache answers. + + +That single sentence pre-empted dozens of downstream questions. _Should we cache classifications?_ Yes. _Should the LLM ever be called for a transaction it has classified before?_ No, unless the user invalidates. _Should the cache generalize?_ Yes, via periodic generalization passes that propose `PREFIX_MATCH` and `REGEX_LEARNED` entries. _Should user corrections be sticky?_ Yes — `user_validated` is the only immutable flag in classification. All of that flowed from one design principle, decided once. + +This is where the 1M context window starts paying for itself. The thesis I wrote at hour three was still loaded at hour thirty. The model did not reinvent the cache shape in a fresh session because there was no fresh session. Every new file got generated with full awareness of every previous file. That's not magic — it's what happens when you stop fragmenting context across chats. + +### 4. Create + +Only now does code get written. And here's the part vibe coders miss: writing code is the easy half. By the time I asked for the first `BancolombiaSavingsParser`, the model didn't need much. It had the scope, the architecture, the taxonomy, the constraints. The prompt was three lines. The output was almost shippable. + +', + content: 'Generate BancolombiaSavingsParser per the BankAdapter contract.', + }, + { + type: 'input', + prompt: 'me>', + content: + 'Use Apache POI poi-ooxml-lite. Handle Colombian-locale amount strings (1.234.567,89).', + }, + { + type: 'input', + prompt: 'me>', + content: 'Walk rows forward to infer year on D/MM dates; skip repeated header bands.', + }, + { + type: 'input', + prompt: 'me>', + content: 'Accept rows with raw_description "0" — pass through, do not drop.', + }, + { type: 'divider', content: '' }, + { type: 'comment', content: 'Output: ~140 lines of Kotlin, compiled first try.' }, + { + type: 'comment', + content: 'Not because the model was clever — because the question was specific.', + }, + { + type: 'comment', + content: 'Specifics flowed from hours of identify/planify/structure work upstream.', + }, + ]} +/> + +The model is a very good autocomplete on top of a plan. It is a very bad architect. Ask "build me a finance app" and you get a vibe. Ask "implement this contract, with these constraints, against this fixture, per the architecture we agreed at hour three" and you get code. + +## What the 1M Context Window Actually Did + +Now the part everyone wants. The single-conversation, 1M-token thing. + +It did not write better code. Opus writes the same Kotlin in a 200K session as it does in a 1M session. What 1M gave me was **continuity** — the elimination of every micro-tax that fragments AI development across sessions. + + + +The big number is not the token count. The big number is the zero in "zero re-primes." Every session boundary in classic AI development is a place where context decays, decisions get re-explained badly, and the model gradually starts contradicting itself. The 1M context window does not make the model smarter. It removes the failure mode where the model forgets what it agreed to four hours ago. + +That said: 1M context is not a license to vibe code longer. If your first three hours are bad, the next twenty-seven are bad too, with full fidelity. The window is a multiplier. It multiplies whatever you put in. + +## The Moments I Said No + +The cleanest signal that you are vibe engineering instead of vibe coding is how often you reject the model's suggestions. Mine, for this project, in order: + +` annotation rows, and savings/CC payment pairs give the effective rate. Statement-derived TRM keeps the system fully reconstructable from source files. No network. Ever.', + }, + ]} +/> + + + +Every "no" was a feature. Saying no is engineering. Saying yes to everything the model proposes is dictation. + +## Where the Plan Earned Its Keep + +The structural decisions made in hours 1–3 paid off everywhere in hours 4–48. A few of the moments where I was glad past-me had decided: + +- **`Account.metadata_json` instead of more columns.** Credit cards have cupo and statement cycle dates; funds have unit value and rentability; loans have rate and term. Different fields per type. Stuffing them as columns produces a sparse, ugly schema; using a typed JSON column kept the relational schema clean. Decided at hour two, never revisited. +- **`source_file_hash` for dedup.** Every import re-checks the SHA-256 against existing `Statement.source_file_hash`. Re-importing the same XLSX is a no-op. This is the kind of correctness property you cannot retrofit after the fact — it has to be in the schema on day one. +- **Single signed `Transaction.amount`.** Two-column debit/credit schemas are double-entry accounting legacy that does not suit a personal-app DB. A single signed decimal is unambiguous and simpler. Decided once, propagated to every metric query for free. +- **`classification_history_json` as append-only log.** Every re-classification appends to the transaction's own audit trail. Five years from now, the user can ask "why is this categorized as Mercado?" and get a chain of decisions, including which prompt version classified it and which user correction overrode it. The cost: one JSON column. The value: auditability forever. +- **Four-layer matching algorithm.** High-confidence auto-match → probable match (review) → LLM arbitration → user confirmation. Designed once, used by both internal transfer detection and loan payment matching. One algorithm, two callers, zero duplication. + +None of these were decisions the model made for me. They were decisions I made with the model. That distinction is the entire point. + +## Useful Is the Only Bar + +Here's the part I want to land hard. **It does not matter whether Mintroot is useful to the community.** It is useful to me. I bank with Bancolombia. I have real XLSX files. I have real money to track. I will use this app every month. If three other people on the internet also use it, that is gravy. If zero do, the project still succeeded the moment I imported my first statement and saw a year of spending categorized in 90 seconds. + +This is the part that gets lost in the "AI lets you ship faster" discourse. Speed is not the unlock. Speed is just the visible part. The actual unlock is that **AI lowers the activation energy for solving real, specific, personal problems** — the long tail of pain points that were never going to justify a startup but absolutely justify a weekend. + +Vibe coders build apps that already exist, badly, because the model defaults to averages. Vibe engineers build apps that don't exist yet, well enough to use, because they brought the problem and used the model for leverage. + + + +If the answer to any of those is no, you did not engineer anything. You generated something. + +## What This Looks Like as a Practice + +If you want to try this on your next project, the loop is small enough to write on a sticky note: + +1. **Find a problem you have this week.** Not a market. A pain. Your own, preferably. Name the victim and the irritation in one sentence. +2. **Write the "not building" list before the "building" list.** The scope you reject is the scope you ship. Lock constraints early; the model will respect them if they exist. +3. **Decide the architecture in conversation, but sign off yourself.** Sketch the modules, the boundaries, the state machines, the data model. The model proposes; you dispose. Three hours of design saves three days of regret. +4. **Only after the first three: generate code.** With full context. With taste applied to every accept. Treat every "do you want me to also..." as a budget decision. +5. **Reject the seductive abstraction.** Two callers is a coincidence; wait for three. Premature factories are how solo projects collapse. +6. **Ship the smallest thing that works on Monday.** Iterate from there or don't. Either is fine. Useful beats complete every time. + +The 1M context window is a force multiplier on this loop. It is not a substitute for it. If you walk in without a plan, you walk out with a longer pile of code that still does not solve your problem — just generated with more continuity. + +## The Pitch + +Mintroot exists because I needed it, scoped it ruthlessly, structured it before generating it, and then used Opus 4.7's 1M context window to hold the whole thing in one conversational head for two days. Not because I am special. Because the four moves are simple and the model is patient. + +**Vibe coding produces artifacts. Vibe engineering produces solutions.** + +The model will happily do either. The choice is yours, every conversation, every accept, every "do you want me to also..." that lands in your chat. Most of those should be no. Most of the rest should be later. The few that survive that filter are the ones worth typing yes to. + +Go solve something specific. Bring the problem. Let the model do the typing. + +Mintroot is open source on [GitHub](https://github.com/Codestz/Mintroot). MIT licensed. Bring your own Gemini key. Built in 48 hours, one conversation, zero vibes. diff --git a/src/content/projects/recurly.mdx b/src/content/projects/recurly.mdx index 7136753..c053515 100644 --- a/src/content/projects/recurly.mdx +++ b/src/content/projects/recurly.mdx @@ -1,25 +1,39 @@ --- -title: "Software Engineer II at Recurly" -description: "Building Recurly Commerce, a subscription management platform integrated with Shopify for recurring revenue businesses" -publishedAt: "2025-07-01" +title: 'Senior Software Engineer at Recurly' +description: 'Building Recurly Commerce, a subscription management platform integrated with Shopify for recurring revenue businesses. Promoted to Senior Software Engineer in May 2026.' +publishedAt: '2025-07-01' current: true -category: "current" -tags: ["shopify", "subscriptions", "saas", "ecommerce", "fullstack"] +category: 'current' +tags: ['shopify', 'subscriptions', 'saas', 'ecommerce', 'fullstack'] featured: true type: 'experience' -author: "Esteban Estrada" -thumbnail: "/images/projects/recurly.jpg" +author: 'Esteban Estrada' +thumbnail: '/images/projects/recurly.jpg' --- -# Software Engineer II at Recurly +# Senior Software Engineer at Recurly -Currently working at Recurly as a Software Engineer II, focused on building and enhancing **Recurly Commerce**, a subscription management platform that helps Shopify merchants manage recurring revenue through subscriptions, memberships, and subscription boxes. +Currently working at Recurly as a **Senior Software Engineer**, focused on building and enhancing **Recurly Commerce**, a subscription management platform that helps Shopify merchants manage recurring revenue through subscriptions, memberships, and subscription boxes. + + ## Experience Overview - **Company**: Recurly -- **Role**: Software Engineer II -- **Timeline**: July 2025 - Present (8 months) +- **Role**: Senior Software Engineer _(promoted from Software Engineer II, May 2026)_ +- **Timeline**: July 2025 - Present - **Location**: Medellín, Antioquia, Colombia - **Product**: Recurly Commerce (Shopify Subscription Platform) - **Tech Stack**: Full-stack web development, Shopify API, Payment Processing @@ -39,6 +53,7 @@ The platform integrates directly into Shopify's native checkout experience and p ## Key Responsibilities ### Shopify Integration Development + - Building and maintaining Shopify app extensions (checkout and customer account integrations) - Developing seamless integration between Recurly Commerce and Shopify's native checkout - Implementing webhook handlers for Shopify events (orders, products, customers) @@ -46,6 +61,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Working with Shopify's GraphQL Admin API for store data access ### Subscription Management Features + - Developing subscription lifecycle management (create, pause, skip, cancel) - Building bundle configuration systems (gift boxes, fixed bundles, customizable options) - Implementing pricing models (subscribe and save, tiered pricing, usage-based) @@ -53,6 +69,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Developing subscription modification flows (swaps, add-ons, upgrades/downgrades) ### Customer Portal Development + - Building low-code customer portal for subscription self-service - Implementing skip and swap functionality for subscription flexibility - Creating gift subscription management features @@ -60,6 +77,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Ensuring mobile-responsive portal experience ### Payment Processing & Billing + - Working with payment processing pipelines for recurring billing - Implementing retry logic for failed payments - Developing dunning management for recovering failed charges @@ -67,6 +85,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Handling proration calculations for subscription changes ### Analytics & Reporting + - Building analytics dashboards for merchant insights - Implementing metrics tracking (MRR, churn rate, LTV) - Creating executive reporting features @@ -74,6 +93,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Integrating with third-party analytics platforms (Klaviyo, Fivetran) ### Churn Reduction Features + - Developing retention tools (pause subscriptions, incentives) - Building cancellation flow optimizations - Implementing win-back campaigns and offers @@ -83,9 +103,11 @@ The platform integrates directly into Shopify's native checkout experience and p ## Key Challenges & Solutions ### Challenge 1: Shopify API Rate Limiting + **Problem**: High-volume operations (bulk imports, sync operations) hitting Shopify API rate limits. **Solution**: + - Implemented request queuing with exponential backoff - Built batch processing for bulk operations - Added caching layer for frequently accessed data @@ -93,9 +115,11 @@ The platform integrates directly into Shopify's native checkout experience and p - Optimized API calls to use bulk operations where possible ### Challenge 2: Subscription State Synchronization + **Problem**: Keeping subscription state consistent between Recurly Commerce and Shopify (orders, inventory, customer data). **Solution**: + - Implemented event-driven architecture with webhooks - Built idempotent webhook handlers to prevent duplicate processing - Created conflict resolution strategies for out-of-sync states @@ -103,9 +127,11 @@ The platform integrates directly into Shopify's native checkout experience and p - Implemented retry mechanisms with dead letter queues ### Challenge 3: Complex Pricing Calculations + **Problem**: Handling various pricing models (tiered pricing, promotions, trials, proration) with correct tax calculations. **Solution**: + - Built comprehensive pricing engine with rule-based calculations - Implemented preview functionality for subscription changes - Created extensive test coverage for pricing scenarios @@ -113,9 +139,11 @@ The platform integrates directly into Shopify's native checkout experience and p - Documented pricing logic for support team reference ### Challenge 4: Checkout Extension Performance + **Problem**: Checkout extensions need to load fast to not impact conversion rates. **Solution**: + - Optimized bundle size for checkout extensions - Implemented lazy loading for non-critical features - Added performance monitoring and alerting @@ -125,6 +153,7 @@ The platform integrates directly into Shopify's native checkout experience and p ## Skills Developed **Technical Skills**: + - Shopify app development and ecosystem - Subscription billing system architecture - Payment processing and PCI compliance @@ -133,6 +162,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Multi-tenant SaaS architecture **Domain Knowledge**: + - Subscription business models and metrics (MRR, churn, LTV) - E-commerce best practices - Recurring payment processing @@ -140,6 +170,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Customer retention strategies **Tools & Platforms**: + - Shopify Admin API (GraphQL & REST) - Shopify App Extensions - Third-party integrations (Klaviyo, Postscript, Gorgias) @@ -147,6 +178,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Analytics platforms **Soft Skills**: + - Working on a product used by thousands of merchants - Balancing merchant needs with platform scalability - Cross-functional collaboration with product, design, and support teams @@ -156,18 +188,21 @@ The platform integrates directly into Shopify's native checkout experience and p ## Impact & Results ### Platform Performance + - Maintaining high availability for subscription processing (critical for recurring revenue) - Fast checkout extension load times to prevent conversion drop-off - Reliable webhook processing for real-time sync - Scalable architecture handling thousands of subscription operations daily ### Merchant Experience + - Simplified subscription setup process for new merchants - Intuitive admin interface for managing subscriptions - Comprehensive analytics for business insights - Flexible subscription options to match various business models ### Customer Experience + - Seamless Shopify-native checkout experience - Easy-to-use customer portal for subscription management - Reliable subscription deliveries and billing @@ -176,6 +211,7 @@ The platform integrates directly into Shopify's native checkout experience and p ## Technical Highlights **Shopify Integration Architecture**: + - OAuth 2.0 authentication for merchant stores - Webhook subscriptions for real-time updates - App extensions embedded in Shopify admin and checkout @@ -183,6 +219,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Metafield management for subscription data **Subscription Engine**: + - Recurring billing scheduler with timezone handling - Subscription lifecycle state machine - Flexible pricing rule engine @@ -190,6 +227,7 @@ The platform integrates directly into Shopify's native checkout experience and p - Automated notification system **Third-Party Integrations**: + - Klaviyo for email marketing automation - Postscript for SMS notifications - Gorgias for customer support context