Skip to content

Migration Experiments

PearlThoughts conducted a proof-of-concept migration extracting core ERPNext accounting modules into idiomatic Go. The approach validated the patterns that became ModernizeSpec’s specification design.

The migration follows the Strangler Fig Pattern with hexagonal architecture (ports and adapters):

  1. Identify a Python source file (doctype controller or shared controller)
  2. Extract the domain model into Go structs
  3. Define port interfaces for all external dependencies
  4. Implement business logic in pure Go
  5. Write table-driven tests covering all rules
  6. Generate a parity report comparing Python and Go behavior
+---------------------------------------+
| Business Logic |
| (validation, calculation, rules) |
+----------+--------------+-------------+
| Ports | Ports | Ports |
| (in) | (out) | (out) |
+-----+----+-------+------+------+------+
| | |
+-----v----+ +----v------+ +---v-------+
| HTTP/ | | Database | | External |
| gRPC | | Adapter | | Services |
+----------+ +-----------+ +-----------+

Dependencies on Stock, Selling, HR, and other modules are abstracted through Go interfaces (ports) and mocked in tests. This allows extracting modules one at a time without building the entire system.

Python source: mode_of_payment.py (66 lines)

Go FilePurposeLines
model.goModeOfPayment struct, ModeOfPaymentAccount child table, PaymentType enum43
validation.go3 validation methods + orchestrating Validate()130
validation_test.go19 table-driven tests, 85.3% coverage345
  • Duplicate company detection in accounts child table
  • Account-company match validation via AccountLookup interface (port)
  • POS profile usage check via POSChecker interface (port)
AspectPythonGoStatus
Duplicate company detectionfrappe.throw() in validate_repeating_companies()ValidationError in ValidateRepeatingCompanies()Verified (19 tests)
Account-company matchfrappe.db.get_value() + frappe.throw()AccountLookup interface + ValidationErrorVerified
POS profile constraintfrappe.db.sql() + frappe.throw()POSChecker interface + ValidationErrorVerified
Test coverage0 tests in Python19 tests in GoGo exceeds Python

Python source: taxes_and_totals.py (1,334 lines)

Go FilePurposeLines
model.goTax-related structs, currency types188
calculator.goIndian GST calculation, multi-currency, threshold rules442
calculator_test.go24 table-driven tests, 90.2% coverage610
RuleDescription
5 charge typesActual, OnNetTotal, OnPreviousRowAmount, OnPreviousRowTotal, OnItemQuantity
Per-item tax overridesJSON item_tax_rate field parsing and application
Multi-currencyUSD to INR at configurable exchange rates
GST splitCGST/SGST calculation for Indian tax compliance
Cascading taxesTax-on-tax with fixed shipping and deductions
Precision replicationFlt() and Round() replicating frappe.utils.flt() behavior

This iteration proved that complex financial calculation logic can be extracted with full behavioral parity, including edge cases around floating-point precision that are notoriously difficult to replicate across languages.

Python source: general_ledger.py (878 lines) — the most complex iteration.

Go FilePurposeLines
model.goGLEntry (30+ fields), PaymentLedgerEntry, Account, GLMap224
ports.go8 port interfaces for all external dependencies180
errors.go7 typed error definitions141
engine.goGL posting engine with dual-entry validation634
engine_test.go25 unit tests818
integration_test.go7 integration tests with realistic ERPNext data611

AccountLookup

Account details, disabled/frozen/group status

CompanySettings

Round-off accounts, frozen dates, cost centers

AccountingPeriodChecker

Closed period validation

FiscalYearLookup

Fiscal year from posting date

GLEntryStore

Save/retrieve/cancel GL entries

PaymentLedgerStore

AR/AP payment ledger entries

BudgetValidator

Budget enforcement

AccountingDimensionProvider

Dimension offsetting entries

MakeGLEntries() — the main entry point that all financial transactions in ERPNext call — plus:

  • Entry merging (combining entries for the same account)
  • Debit/credit toggle
  • Disabled account validation
  • Accounting period checks
  • Rounding
  • Cancellation reversals
  • Payment ledger creation
MetricValue
Python source analyzed~2,278 lines from 3 files
Go code produced~4,366 lines (implementation + tests)
Total tests68 (all passing)
External dependenciesZero (stdlib only)
Test-to-implementation ratio~1.15:1
Code expansion factor~1.9x (Go requires more explicit code than Python)
IterationPython SourceGo OutputTestsCalendar Time
Mode of Payment66 lines518 lines19~1 day
Tax Calculator1,334 lines1,240 lines24~1 day
GL Entry Engine878 lines2,608 lines32~1 day

Rate: approximately 760 lines of Python source processed per day per person (experienced engineer with AI tooling).

DimensionMigratedRemaining
Accounts doctypes3~289
Other module doctypes0~229
Shared controllers1 partial (taxes_and_totals)15
API endpoints0768
Reports0~177
Frontend (JS)073,932 lines
Framework (Frappe)0Entire runtime

The 3 iterations migrated architecturally central components — Mode of Payment (master data), Tax Calculator (shared controller), and General Ledger (accounting core). These represent the most complex per-line code in ERPNext.

Every aspect of this experiment maps to a ModernizeSpec concept:

Experiment ConceptModernizeSpec Equivalent
Identifying extraction targetsextraction-plan.json phases and sequencing
Measuring complexity per modulecomplexity.json tiers and hotspots
Mapping bounded contextsdomains.json context definitions
Tracking what is migratedmigration-state.json per-context progress
Parity test resultsparity-tests.json test inventory
Port interfaces as boundariesdomains.json coupling scores

The specification formalizes what was ad-hoc during the experiment, making the approach repeatable for any legacy system.