When a Product Brief Wants a New Service Before the System Map Exists
At the architecture readout, the product brief showed a clean set of customer-facing pages ready for a new Go service. The first preference check broke that picture: only the existing Django application could interpret the value and retrieve it from the profile database. That was where the discussion about microservice boundaries stopped being abstract.
TL;DR
Product grouping does not establish microservice boundaries. Before extraction, identify who owns each capability, which system is authoritative for its data, what happens when dependencies fail, and who carries operational responsibility. A staged decision can preserve current authority while making the cost of latency, replication, or eventual ownership transfer explicit.
The page boundary was not the system boundary
The customer wanted to rebuild several search and discovery experiences. The existing implementation lived in a mature Django application, while the proposed Go service had already received some exploration.
The decomposition looked plausible because the pages shared a customer purpose and could be developed together. Yet a page is a presentation boundary. It does not establish where identity is resolved, which system authorizes preferences, how localized content is selected, who corrects bad data, or where administrative changes take effect.
The dependency inventory exposed the difference. Search behavior relied on data whose authority was not consistently documented. Preferences affected results but remained part of the existing Django application and its profile database. Localization included selection and fallback behavior, not merely translated labels. Administrative workflows and operational procedures were still embedded in the existing Django application.
Routing traffic to the proposed Go service would move an entry point, while authority and failure responsibility remained in the existing Django application. A credible service boundary eventually needs both to move deliberately. During a staged extraction, leaving them in Django can be the safer decision, provided the runtime dependency is explicit.
This aligns with Microsoft's microservices architecture guidance, which describes services around cohesive business capabilities and ownership of domain logic and data. The implementation language matters after the ownership model is credible.
Testing microservice boundaries with one preference read
Consider an explicitly illustrative request. A browser asks the proposed Go service for a customer-facing page. Rendering that page requires one saved preference. The preference is currently owned by the existing Django application and stored in the profile database. The proposed Go service therefore cannot answer independently unless the architecture changes.
The runtime dependency is defensible only when its stage, owner, and failure behavior are explicit.
The smallest stage is a synchronous adapter. The proposed Go service requests the preference from the existing Django application, which remains authoritative and continues to read the profile database. This preserves one source of truth and avoids premature data movement. It also adds network latency and couples page availability to the existing Django application. Timeouts, load limits, error semantics, observability, and degraded behavior become part of the contract.
More runtime autonomy requires a replicated read model. Preference changes could be published and projected into storage available to the proposed Go service. Reads would no longer require the existing Django application to be available. In exchange, the architecture acquires freshness targets, replay procedures, reconciliation checks, schema evolution rules, event-contract ownership, and a policy for missing or out-of-order changes. Engineers would need evidence that the customer experience can tolerate stale values and that the projection can be rebuilt safely.
Transferring ownership is a different class of decision. Moving the preference capability would require a migration plan covering existing records, validation, write ownership, administrative behavior, consent semantics, rollback, and a controlled cutover. Casual dual writes are especially hazardous: if both applications accept authoritative changes, conflicting order, partial failure, and retries create two authorities. Dual writing is defensible only with rigorous reconciliation, an explicit conflict policy, measurable convergence, and a bounded retirement plan.
The review treated these as possible stages, not completed work. The choice turns on required availability, acceptable staleness, migration cost, and whether preference ownership belongs inside the proposed boundary at all.
Turn uncertainty into a decision artifact
When no complete map exists, waiting for comprehensive documentation is rarely useful. Start with the smallest dependable baseline for the decision. Discovery Baselining When Nobody Owns the Whole System Map describes the broader practice. For this decision, the baseline should become a capability-and-boundary matrix.
| Capability | Current authority | Reads and writes | Runtime dependency | Failure responsibility | Evidence required | Staged disposition |
|---|---|---|---|---|---|---|
| Search presentation | Existing Django application | Reads records, identity context, locale, and preferences | Existing application behavior and upstream data | Page degradation, latency, and result correctness | Load profile, fallback behavior, parity criteria | Candidate for isolated presentation work |
| Identity context | Existing Django application | Reads identity and authorization state | Existing identity behavior | Authentication errors, authorization drift, incident escalation | Stable contract, timeout policy, operator, audit expectations | Keep authority in place |
| Preferences | Existing Django application and profile database | Reads and writes saved choices and consent-sensitive state | Synchronous adapter or a future replicated model | Stale values, failed reads, incorrect writes, reconciliation | Authoritative write path, freshness target, change contract, correction procedure | Stage the read; do not transfer ownership yet |
| Localization | Existing Django application | Reads locale, content, and fallback rules; administration updates content | Existing localization behavior | Wrong locale, missing content, fallback mismatch | Update workflow, cache behavior, fallback parity, operator | Preserve current behavior behind an explicit contract |
| Administration | Existing Django application | Writes managed records, permissions, and audit state | Existing administrative workflows | Unauthorized change, incomplete audit history, support failure | Workflow inventory, permission model, audit retention, support owner | Keep with current authority |
| Search data | Authority not yet reduced to one system | Reads source records; corrections may originate elsewhere | Source and correction paths | Stale results, conflicting corrections, incomplete recovery | System of record, freshness objective, correction owner, recovery procedure | Defer ownership decision |
The matrix should be challenged row by row. Which value wins when systems disagree? Who corrects it? What must remain behaviorally identical during a transition? What is the availability impact of every synchronous call? Can replicated state be replayed and reconciled? Which operator receives an alert, and what action can that operator take?
An unknown entry is useful because it creates discovery work. An unstated assumption is dangerous because it becomes architecture without a decision. In practice, the hardest conversation is often not about technology. It is asking someone to accept responsibility for a failure mode that previously belonged elsewhere.
A credible assessment of microservice boundaries should produce three outcomes: work that can proceed under current authority, work blocked by missing evidence, and work that requires a deliberate ownership migration. This keeps a page rebuild from quietly absorbing data migration, operational transfer, and administrative replacement.
The readout did not approve a wholesale extraction. It separated presentation work that could continue from dependencies that still needed evidence and ownership changes that required a later decision. The visible architecture became less tidy, but the decision became more honest.
The approach has a real cost. Clarifying authority delays visible delivery, and a synchronous stage creates temporary runtime coupling between the proposed Go service and the existing Django application. Those costs should be stated directly and compared with the operational and data-integrity risks of choosing a boundary that exists only in a deployment diagram.
FAQ
What evidence is needed before extraction?
At minimum, establish the authoritative system for each required value, the read and write paths, behavioral parity requirements, availability and freshness expectations, correction procedures, migration constraints, and named operational responsibility. Evidence should include observed call paths and data flows, not only intended architecture.
Is a shared database acceptable?
It can be a constrained transitional choice, but it weakens autonomy and makes schema changes a cross-boundary event. Direct access by the proposed Go service to the profile database would also bypass behavior and authorization held by the existing Django application. If temporary shared access is considered, define permitted tables and operations, schema-change coordination, security controls, observability, failure ownership, and an exit condition.
How should work proceed when the system map is incomplete?
Map only the capabilities and dependencies needed for the next decision, record unknowns in the matrix, assign evidence owners, and separate reversible presentation work from authority changes. Use a synchronous adapter when its coupling is explicit and acceptable, while gathering the evidence needed to choose replication, transfer, or continued ownership.
A boundary becomes credible when the responsibility on each side is clearer than the box drawn around it.
