Khi nào rewrite, khi nào refactor, khi nào archive: Bài học từ HashiCorp Boundary và Total Annihilation
Phân tích quyết định kiến trúc qua case study thực tế: HashiCorp archive Boundary, game engine Total Annihilation được viết lại. Khi nào chi trả tech debt, khi nào cắt bỏ, khi nào đầu tư tái cấu trúc.
· 6 min read

Ảnh: DTankersley (WMF), BY-SA qua openverse (nguồn)
HashiCorp vừa archive Boundary. Dự án open source quản lý privileged access này sinh ra năm 2020, hứa hẹn thay thế VPN truyền thống bằng identity-based access. Sáu năm sau, repo chuyển sang read-only. Không có announcement lớn, không có migration path rõ ràng. Chỉ một commit đơn giản: “Archive this repository.”
Cùng tuần, community game dev tái hiện Total Annihilation — RTS kinh điển năm 1997 — bằng engine hiện đại. Dự án nanolathe.gg không chỉ port sang 64-bit. Họ viết lại toàn bộ simulation loop, rendering pipeline, networking stack. Kết quả: game chạy mượt trên hardware ngày nay, hỗ trợ modding qua Lua, multiplayer deterministic across platforms.
Hai sự kiện, hai chiều hướng đối lập. Một bên dừng lại, một bên đầu tư all-in vào rewrite. Vấn đề không phải ở quyết định đúng hay sai. Vấn đề ở chỗ: team nào cũng nghĩ mình đang chọn đúng lúc đó. Thực tế là hầu hết đều đánh giá sai chi phí opportunity cost.
Vấn đề ở chỗ: Tech debt không phải là nợ tài chính
Nhiều lead treat tech debt như nợ ngân hàng: có principal, có interest, trả dần được. Sai. Tech debt là architectural coupling. Khi bạn defer một refactor, bạn không chỉ nợ code. Bạn nợ constraint cho mọi feature sau đó.
Boundary của HashiCorp có architecture dựa trên OPA (Open Policy Agent) cho authorization decisions. Design này clean trên giấy: separation of concerns, policy as code, testable. Nhưng OPA thêm latency cho mỗi request. Với privileged access — nơi user chờ 200ms đã phàn nàn — latency ấy thành blocker. Team phải workaround: caching, pre-evaluation, fallback paths. Mỗi workaround thêm complexity. Sau ba năm, codebase trở thành tập hợp các exception cases.
Tôi từng gặp case tương tự tại dự án trước. Team adopt event sourcing cho order domain. Six months sau, projection rebuild mất 4 giờ. Mỗi schema change phải replay toàn bộ event store. Business yêu cầu feature mới mỗi sprint. Team chọn shortcut: thêm column trực tiếp vào read model, bypass event store. Kết quả: hai source of truth, reconciliation job chạy nightly, bug data inconsistency mỗi tuần.
Điểm mấu chốt: tech debt không compound linearly. Nó compound qua architectural decisions mà team không thể revert dễ dàng. Boundary’s OPA choice không phải bug. Là architectural commitment. Khi commitment đó không fit scale requirement, cost để undo không phải refactor — là rewrite từ đầu.
Khi nào rewrite: Total Annihilation case study
Total Annihilation rewrite thành công vì ba yếu tố hội tụ:
1. Specification frozen. Game logic năm 1997 không đổi. Unit stats, physics formulas, map format — tất cả documented, tested bằng regression suite so với binary gốc. Không có stakeholder yêu cầu “thêm feature này cho sprint tới”.
2. Clear boundary. Engine rewrite tách biệt hoàn toàn khỏi game data. Team dùng original .hpi files, .ufo maps, .tdf unit definitions. Chỉ simulation và rendering bị thay thế. Domain logic (unit behavior, economy) giữ nguyên.
3. Deterministic verification. Mỗi frame phải match bit-by-bit với original tại cùng tick. Property-based testing catch divergence sớm. CI run thousands of replay files nightly.
Điều này map trực tiếp đến DDD: bounded context rõ ràng, ubiquitous language không đổi, domain model stable. Rewrite chỉ ở infrastructure layer — rendering, networking, memory management. Domain layer untouched.
So sánh với Boundary: HashiCorp không có luxury frozen spec. Enterprise access requirements thay đổi liên tục: just-in-time access, break-glass procedures, compliance auditing, multi-cloud identity federation. Mỗi requirement mới push architecture ra ngoài boundary ban đầu.
Khi nào refactor: Strangler Fig pattern thực chiến
Refactor an toàn khi bạn có seam — điểm cắt để thay thế incremental. Strangler Fig pattern: tạo facade, route traffic dần sang implementation mới, decommission old code.
Tôi áp dụng pattern này cho payment gateway migration năm ngoái. Legacy system: monolithic PHP, direct DB calls, no tests. New system: Go services, event-driven, contract testing.
Bước 1: Tạo adapter layer. Tất cả calls từ frontend qua adapter. Adapter route 100% sang legacy. Bước 2: Implement một use case mới — refund processing. Viết contract test trước. Deploy canary 5% traffic. Bước 3: Mỗi sprint migrate một use case. Authorization, capture, void, webhook handling. Bước 4: Khi legacy chỉ còn read-only reporting, tắt hẳn.
Kết quả: 14 tháng, zero downtime, zero data loss. Quan trọng: business không bao giờ bị block. Feature mới vẫn ship trên legacy trong khi migration chạy song song.
Refactor fail khi team cố gắng “rewrite module by module” mà không có seam. Ví dụ: extract service từ monolith nhưng shared database vẫn còn. Distributed transactions thay vì local. Chạy song song hai năm, cuối cùng merge lại vì operational burden quá lớn.
Khi nào archive: Decision framework
Archive không phải failure. Là strategic decision. HashiCorp archive Boundary vì:
- Market shift: Zero Trust Network Access (ZTNA) dominated by cloud vendors (Cloudflare Access, Tailscale, Twingate). Boundary positioned giữa self-hosted và managed — không win được cả hai.
- Maintenance cost > value delivered. Team size nhỏ, security surface lớn, compliance burden cao.
- Strategic pivot: HashiCorp focus vào Vault, Terraform, Consul — core revenue drivers.
Framework tôi dùng cho team khi evaluate archive vs invest:
Archive nếu:
- No active users paying hoặc strategic value
- Maintenance burden blocks higher-ROI work
- No clear path to product-market fit trong 12-18 tháng
- Security/compliance risk vượt quá capacity team
Invest (refactor/rewrite) nếu:
- Clear revenue tie hoặc strategic moat
- Stable domain model, changing infrastructure needs
- Team có capacity và expertise để execute
- Migration path có measurable milestones
Sunset gracefully nếu:
- Có users nhưng không justify investment
- Provide migration tooling, timeline 6-12 tháng
- Open source: handover to community hoặc fork-friendly license
- Document lessons learned cho future decisions
Testing strategy cho architecture decisions
Điều tôi thấy thiếu ở nhiều team: architecture decision records (ADR) có testable criteria.
Với Boundary, ADR có thể ghi: “Choose OPA for policy evaluation. Acceptable latency < 50ms p99 at 10k req/s. Review at 6 months.” Test: load test monthly. Nếu p99 > 50ms liên tục 3 tháng → trigger reevaluation.
Với payment gateway migration, ADR: “Strangler Fig migration. Success criteria: zero regression bugs in production, latency p99 < 200ms, error rate < 0.01%.” Test: contract test suite run trên every PR. Canary metrics dashboard alert nếu vượt threshold.

Ảnh: Thomas Ernsting, BY-SA qua openverse (nguồn)
Testing architecture decisions khác unit testing. Unit test verify implementation correctness. Architecture test verify decision validity over time. Cần:
- Contract tests cho service boundaries
- Load tests cho performance assumptions
- Chaos tests cho resilience assumptions
- Regression tests cho behavioral compatibility (như Total Annihilation replay suite)
- Decision review cadence — quarterly ADR review meeting
Team mình hiện tại dùng lightweight ADR format: Markdown file trong repo, có status: proposed | accepted | deprecated | superseded, có test-criteria section, có review-date. CI check: nếu ADR quá review-date mà chưa update → fail build. Buộc team confront decision.
Kết luận actionable
Làm việc với legacy là daily job của senior engineer. Không có silver bullet. Nhưng có framework:
- Map bounded contexts trước. Nắm rõ domain nào stable, domain nào volatile. Rewrite chỉ infrastructure layer của stable domains.
- Tìm seam. Nếu không có seam → tạo seam trước (facade, adapter, anti-corruption layer). Không bao giờ rewrite blind.
- Define testable criteria cho mọi ADR. Architecture decision không có test criteria là assumption, không phải decision.
- Review cadence. Quarterly review ADR. Deprecate những cái không còn valid. Archive những cái không còn ROI.
- Communicate trade-offs honestly. Business hiểu: refactor tốn 20% capacity, rewrite tốn 6-18 tháng, archive có migration cost. Cho họ quyết định cùng.
HashiCorp archive Boundary không phải thất bại. Là acknowledgment rằng market đã shift, architecture commitment không còn serve purpose. Total Annihilation rewrite thành công vì team respect domain stability, invest vào verification tooling, không over-engineer.
Lần tới team bàn “should we rewrite this?” — hãy hỏi: Domain model này có stable không? Chúng ta có seam không? Test criteria là gì? Review date khi nào? Nếu trả lời không được cho tất cả → đừng rewrite. Refactor incremental hoặc archive.
Tech debt không phải nợ. Là architectural constraint. Quản lý nó bằng engineering discipline, không phải wishful thinking.

Ảnh: Ndahiro derrick, CC BY 4.0 qua wikimedia (nguồn)