Appearance
Decision Log
Phase: Architecture decisions for staged rewrite Status: active; decisions govern implemented foundations and future proof-gated route promotion.
D-001: Use Strangler Fig Through API Gateway
Decision: Build api-gateway first and keep /api/* compatibility while route groups migrate individually.
Reason: Frontend is tightly coupled to current /api paths and response shapes. A big-bang backend replacement would make import, exam, and attempt behavior hard to verify.
Status: accepted.
D-002: Keep Legacy Source Read-Only
Decision: Legacy source is audit evidence only. New implementation lives under go-platform/.
Reason: The rewrite must not destabilize the current product.
Status: accepted.
D-003: Use Database Per Service
Decision: Every target Go service owns a separate Postgres database.
Reason: The target architecture is microservice-oriented, and sharing the legacy Prisma schema would preserve the current coupling.
Status: accepted.
D-004: Preserve /api Contract Before Creating Clean /v1 Clients
Decision: Native services may expose /v1/..., but public frontend traffic remains /api/... through gateway adapters.
Reason: Frontend edits are out of scope and current client code calls /api directly.
Status: accepted.
D-005: Wrap Existing Go Formula DOCX Behavior First
Decision: docx-import-service initially wraps or reproduces the current Go Formula DOCX endpoint contract before attempting parser improvements.
Reason: DOCX import correctness depends on warnings, media, formulas, table data, and editor review behavior.
Status: accepted.
D-006: Keep Wallet/AZ Credit Legacy-Proxied
Decision: Wallet and AZ Credit public routes remain legacy-proxied until their owner-service route proof is accepted. wallet-service, billing-service, usage-service, and payment-service are canonical target services.
Reason: Monetization has irreversible billing and entitlement effects. Native foundations do not authorize public/default route promotion without contract, data, runtime, rollback, and browser proof.
Status: accepted.
D-007: Do Not Normalize Exam/Attempt Snapshots Away
Decision: Exam and attempt services preserve snapshot JSON fields during initial migration.
Reason: Snapshots protect historical attempts from later question edits and are central to grading parity.
Status: accepted.
D-008: Redis First For Compatibility, NATS Preferred For New Events
Decision: Redis remains acceptable for early local/legacy compatibility; NATS is preferred for new event bus if added in Phase 2.
Reason: Legacy workers and SSE flows already depend on Redis. NATS gives cleaner service event semantics, but migration should not block on it.
Status: accepted.
D-009: Taxonomy Initially Owned By Question Bank
Decision: Shared taxonomy reference data starts under question-bank-service.
Reason: Taxonomy is most tightly coupled to questions/import/exams. Other services consume by API or replicated read model.
Status: accepted pending later school-domain review.
D-010: DOCX Import Uses Opt-In Go Formula Adapter
Decision: docx-import-service uses the Go Formula DOCX adapter when GO_FORMULA_DOCX_URL or GO_FORMULA_DOCX_BASE_URL is configured; otherwise it returns a warning-only baseline response.
Reason: The current Go adapter calls the full /v1/import/docx/qas envelope, retaining parser schemaVersion, formula provenance, warning, media, and review fields. The external runtime's compact /v1/import/docx/simple endpoint remains available for clients that need a reduced payload, but it is not the import-service adapter contract. Keeping the adapter opt-in lets local skeletons boot without requiring the external parser.
Status: accepted.
D-011: DOCX Import Jobs Use Repository Interface First
Decision: DOCX import job/status/event endpoints use a repository interface with pgx-backed Postgres when DATABASE_URL is reachable and an in-memory fallback for local skeleton runs without a database.
Reason: The target state requires docx-import-service to own hoctapaz_import_db, but early local development must still run without requiring the full database stack. The fallback is operationally visible in logs and does not mutate legacy state.
Status: accepted.
D-012: Document Service Starts With Media Metadata Ownership
Decision: document-service owns media asset metadata in hoctapaz_document_db.media_assets and exposes legacy-shaped /v1/storage/* compatibility endpoints before gateway cutover.
Reason: DOCX import image materialization and import-editor media parity need a stable MediaAsset boundary before docx-import-service writes extracted image references. The storage adapter is S3-compatible for MinIO/R2-style deployments, while tests use an in-memory implementation behind the same interface. Image content variants mirror the legacy trim/formula contract for stdlib-decodable images and fall back to original bytes if transformation fails.
Status: accepted.
D-013: Keep Upload Quota Enforcement Out Of Early Document-Service Cutover
Decision: The Go document-service mirrors legacy upload MIME, size, key, and response contracts, but leaves wallet/AZ Credit quota enforcement on legacy routes until the billing boundary is explicitly migrated.
Reason: Legacy StorageController.presigned and AppDataService.createMediaAsset call AzCreditService.assertCanConsume. Billing, wallet, usage, and payment are target services, but silently moving quota writes into document-service would still bypass their owner contracts and create unreviewed billing side effects.
Status: accepted.
D-014: DOCX Image Materialization Is Best-Effort And Warning-Safe
Decision: docx-import-service materializes Go Formula DOCX image references through document-service only when DOCUMENT_SERVICE_URL is configured and the image bytes can be read from the uploaded DOCX package. Failed materialization preserves the original media reference and emits GO_FORMULA_DOCX_IMAGE_MATERIALIZE_FAILED.
Reason: The target service boundary requires document-service ownership for stored media, but parser output can contain unsupported WMF/OLE assets, missing package paths, or deployment-specific temporary paths. Preserving references with explicit warnings avoids silent data loss while allowing extractable raster images to use the same /api/storage/media-assets/{id}/content path as the legacy editor.
Status: accepted.
D-015: Exam Service Stores Hydrated Question Snapshots
Decision: exam-service stores exam question snapshots from hydrated API payloads and does not join the question-bank-service database.
Reason: Legacy publish and attempt flows rely on questionSnapshotJson and optionOrderJson to keep exam/attempt behavior stable after question edits. Database-per-service migration requires that stability to be preserved through service APIs, not cross-service SQL.
Status: accepted.
D-016: Deploy Go Formula DOCX As An Internal Runtime Dependency
Decision: Local Compose, offline K8s, and Helm deployments build the source-controlled runtimes/go-formula-docx module as an internal go-formula-docx runtime. It remains outside the standard service inventory.
Reason: docx-import-service depends on the full /v1/import/docx/qas response and retains its versioned QAS schema and formula provenance for import review. Treating the parser as a runtime dependency preserves isolated parser ownership while making DOCX Fast and QAS imports operational in local/offline deployments. It remains outside SERVICES because it does not own a service database, migration job, or standard /metrics contract.
Status: accepted.
D-017: Use NATS JetStream For Monetization Broker Transport
Decision: Monetization events use NATS JetStream as the shared broker transport for the first non-HTTP event bus implementation.
Reason: D-008 already prefers NATS for new service events while keeping Redis for legacy/BullMQ compatibility. Payment, billing, wallet, and usage services already have service-owned outboxes and replay-safe consumers, and the platform already ships NATS in Docker Compose, offline K8s, Helm values, and platform config. JetStream gives durable stream storage, explicit consumers, redelivery/replay, and dead-letter workflow hooks without coupling monetization to Redis queues that still mirror legacy worker behavior.
Scope:
- Stream:
MONETIZATION. - Subjects:
monetization.payment.order.paid.v1monetization.billing.entitlements.updated.v1monetization.billing.subscription.activated.v1
- Producers:
payment-servicepublishespayment.order.paid.v1from the existing payment outbox.billing-servicepublishesbilling.entitlements.updated.v1andbilling.subscription.activated.v1from the existing billing outbox.
- Consumers:
billing-serviceandwallet-serviceconsume payment paid events.usage-serviceconsumes entitlement updates.wallet-serviceconsumes subscription activation credit events.
Rules:
- Service-owned outbox rows remain the source of retry, dead-letter, and operator evidence. JetStream publishing is an outbox dispatcher mode.
- Operator replay uses
tools/monetization-outboxagainst the owning payment-service or billing-service database only. The outbox id, event id, and JetStream message id are the same key; retry/replay requeues the existing row and does not create a replacement event. - Existing internal HTTP event endpoints remain rehearsal and rollback fallback surfaces only; they are not the final target transport.
- Consumers persist
sourceServiceplussourceEventIdso JetStream redelivery, HTTP fallback replay, and manual outbox replay converge on the same result. - Local/offline runtime must enable JetStream explicitly with file storage before broker smoke can be treated as runtime proof.
- Public payment, wallet, subscription, and quota routes remain legacy-proxied until broker runtime proof is paired with gateway rehearsal, browser/runtime parity, and rollback evidence.
Status: accepted.