Reverse PRDs: From Legacy Code to Clear Product Specs
Every engineering team has that one system. The payment processor that "just works." The notification engine that nobody touches. The search implementation that predates half the team.
It works. It's critical. And nobody knows exactly what it does.
The original engineers left years ago. The PRD is buried in a Confluence graveyard or never existed. The code is your only source of truth. When someone asks "what does this system actually do?" you get handwaving and tribal knowledge.
This is where reverse PRDs come in. Instead of writing specs before code, you extract specs from code. You work backwards from implementation to intent. It's code archaeology with a product lens.
Code tells you how something works. It doesn't tell you why it exists, what problems it solves, or what edge cases matter. Code is the compiled output of product thinking. Without the original thinking, you're flying blind.
Here's what happens without specs:
You need to modify the payment system. You read through 3,000 lines of transaction handling logic. You see special cases for gift cards, promotional credits, refunds, currency conversion. You have no idea which behaviors are intentional and which are bugs that became features. You're terrified to change anything.
Or someone asks "can we add buy-now-pay-later?" You don't know if the current architecture supports split payments. You don't know what invariants must hold. You don't know who to ask because the knowledge exists only in the code, and the code doesn't explain itself.
This is expensive. Every feature addition takes 3x longer because engineers are reverse-engineering requirements while they code. Every bug fix risks breaking undocumented behavior that some obscure workflow depends on.
The Reverse PRD Process
Start with the API surface. What can external callers do? This is your feature list.
For a payment system, you might find:
processPayment(amount, method, userId)
refundPayment(transactionId, amount)
getPaymentHistory(userId, dateRange)
validatePaymentMethod(method)
Each function represents a capability. Each capability was someone's requirement at some point.
Now dig into implementations. Look for conditional logic. Every if statement is a product decision.
def processPayment(amount, method, userId):
if method.type == "gift_card":
if amount > method.balance:
# Try to split between gift card and backup method
return splitPayment(method.balance, amount - method.balance, userId)
if user.country == "DE" and amount > 1000:
# German regulations require 2FA for large transactions
require2FA(userId)
if isBlackFriday() and method.type == "credit_card":
# Double retry logic during high traffic
maxRetries = 6
This code contains three product requirements that nobody wrote down:
Gift cards should automatically split with backup payment methods
German users must complete 2FA for transactions over €1000
Credit card processing gets extra retry attempts during Black Friday
Extract these into proper requirement statements:
Feature: Gift Card Split Payments
When gift card balance is insufficient, system automatically charges remaining amount to user's backup payment method
Must maintain transaction atomicity (both charges succeed or both fail)
User receives single confirmation for split transaction
Feature: Regulatory Compliance
German users: 2FA required for transactions exceeding €1000 (PSD2 compliance)
System must block payment until 2FA completes
Timeout: 5 minutes, then cancel transaction
Feature: High-Traffic Resilience
Black Friday period (Nov 24-27): increased retry logic for credit cards
Standard: 3 retries, Black Friday: 6 retries
Prevents revenue loss from temporary gateway issues
Now you have specs. They're not beautiful. They're not how you'd write a greenfield PRD. But they capture actual behavior and actual product decisions.
What to Look For
Validation logic reveals requirements. Every validation rule is a constraint someone cared about.
If you see assert amount > 0 and amount < 100000, that's not just defensive programming. Someone decided the system shouldn't process transactions over $100k. Why? Fraud prevention? Technical limitation? Regulatory compliance?
Error handling shows edge cases. Look at what can fail and how failures are handled.
try:
result = paymentGateway.charge(amount)
except GatewayTimeout:
# Queue for retry, don't fail user request
enqueueRetry(transaction)
return "pending"
except InsufficientFunds:
# Immediate failure, notify user
return "declined"
This tells you: timeouts are transient, insufficient funds is permanent. The product treats these failures differently. That's a requirement.
Database schema encodes business rules. Table structures reveal what the product tracks.
If your payments table has columns for original_amount, tip_amount, tax_amount, and total_amount, your product has requirements around itemized transactions. If there's a dispute_deadline timestamp, you handle chargebacks. If there's a settlement_batch_id, you batch process settlements.
Each field represents a product capability or business rule.
Where Glue Fits
Manual reverse engineering works for one system. It falls apart at scale.
Most codebases have dozens of these undocumented systems. Payments, notifications, search, recommendations, authentication, billing, reporting. Each one has implicit requirements locked in code.
Glue indexes your entire codebase and uses AI to discover what each system actually does. It generates documentation from implementation, extracts feature descriptions, and maps business logic to code. You get specs without manually reading thousands of lines.
For that payment system, Glue would:
Identify all payment-related features automatically
Extract conditional logic and document it as requirements
Map which engineers have modified payment code (ownership)
Show code health metrics (complexity, churn, test coverage)
Generate readable specs that explain what the system does
You can query "show me all regulatory compliance logic" and Glue surfaces the German 2FA requirement, the transaction limits, and the audit logging. You can ask "what payment methods do we support?" and get a structured list with implementation details.
Real Example: Notification System
I worked with a team that had a notification system processing 100M+ messages daily. Zero documentation. Original team long gone.
Started reverse engineering. Found the main dispatch logic:
def sendNotification(userId, event, channel):
user = getUser(userId)
if user.preferences.get(event.type) == "disabled":
log("notification_suppressed", event.type)
return
if channel == "email":
if isNighttime(user.timezone):
if event.priority != "urgent":
scheduleForMorning(userId, event)
return
if user.notification_count_today > 50:
if event.priority == "low":
drop(event)
return
elif event.priority == "medium":
combine_with_digest()
return
dispatch(channel, format(event, user))
Extracted these specs:
User Preference System: Users can disable notification types. System respects these preferences across all channels. Suppressions are logged for analytics.
Smart Timing: Email notifications respect user timezones. Non-urgent emails sent during 10pm-8am are delayed until 8am user local time. SMS and push notifications send immediately regardless of time (urgent events only).
Rate Limiting: Users capped at 50 notifications/day. Low-priority notifications dropped after limit. Medium-priority notifications batched into daily digest. High/urgent priority always delivered.
These weren't documented anywhere. But they were critical product behavior. The rate limiting prevented notification fatigue. The timezone logic improved engagement. The preference system was legally required in some regions.
Glue would have extracted this in minutes instead of days.
Making It Stick
Reverse PRDs only work if they become real documentation.
Put them in your docs repo. Link from the code. Make them the canonical description of system behavior. When engineers modify the code, they update the spec. When PMs plan features, they check existing capabilities.
This is the maintenance burden. Specs drift from implementation. You need tooling to keep them synced.
Glue handles this automatically. As code changes, Glue re-indexes and updates documentation. You see when specs diverge from implementation. You get prompted to update docs when modifying features.
It treats docs as a first-class build artifact, not a separate thing that rots.
When Not to Reverse Engineer
Some code is genuinely temporary. Hacky workarounds. Prototypes that escaped. Technical debt you plan to delete.
Don't document these. Don't give them specs. Document them as "slated for removal."
Also, some complexity is accidental. The code does something convoluted, but that's not the requirement. The requirement is simple; the implementation is bad.
Example: I found a notification deduplication system with 400 lines of complex caching logic. The requirement? "Don't send the same notification twice within 24 hours." The implementation was overengineered garbage.
In the reverse PRD, I documented the simple requirement and noted "implementation is unnecessarily complex, consider simplification." Don't confuse implementation complexity with requirement complexity.
The Payoff
Once you have specs for your legacy systems, everything gets easier.
Onboarding: New engineers read what the system does before diving into how. They understand intent before implementation.
Modifications: You know what behavior is intentional. You know what's safe to change.
Migrations: You have requirements to validate against. You can rewrite with confidence.
Product planning: PMs understand existing capabilities. They don't propose features you already have. They don't break behavior people depend on.
Your legacy code stops being scary. It becomes understandable. Still old, still imperfect, but at least documented.
The code already exists. The requirements already exist—in compiled form. Reverse PRDs make them legible again.