Solution Architecture5 November 20268 min read

Architecture Must Earn Its Complexity — Part 4 of 9

One database does not mean shared ownership

Part 4 of Architecture Must Earn Its Complexity. A modular monolith can run on one database server without letting every module touch every table. Schemas, roles, migration ownership, transaction rules and a controlled reporting path keep data ownership real until, and unless, a module leaves.

#Solution Architecture#Data Governance#Architecture Decisions#Microservices#Technical Debt
A compact building of clearly walled modules, with one module moved out on a rail into its own unit and new cables spanning the gap.

Kagiso Seabi's monthly report for the provincial department was the most trusted thing the fictional Magaliesberg Provincial Supplier Desk produced. It showed registrations, documents submitted, verification turnaround and open cases by district. It was also a single SQL query of 280 lines that joined tables from five modules, running under the read-only role that reporting had kept across every schema as a stopgap during the clean-up in Part 2. When the Verification module renamed a status column in a routine release, the report silently started counting every pending verification as complete. It took a district manager's question, three weeks later, to notice.

Nobody had broken a rule. The modules already had their own schemas and roles, but the stopgap meant that, for reporting, the database still behaved as if everything belonged to everyone, and the ownership rules were never written down in full.

The Magaliesberg Provincial Supplier Desk is a fictional provincial supplier-support platform. Its people, systems and figures are illustrative, and all data is synthetic. This part explains how a modular monolith can share a database server without sharing data ownership.

One server, several owners

The common objection to a modular monolith is that "it all ends up in one database anyway". That confuses the physical database with the ownership model. A single database server can hold several schemas, each owned by one module, each accessed through its own credentials, each changed by its own migrations.

One database server, separate ownersRegistrationDocumentsVerificationCasesReportingONE DATABASE SERVERregistrationown roledocumentsverificationcasesreporting viewsdeniedSame server, separate owners: schema, role and migrations per module
Each module connects with its own role, which can use only its own schema, and owns that schema's migrations. A query from Cases into Verification's tables fails with a permission error at run time instead of producing a wrong report weeks later. The fifth schema is a dedicated reporting schema of module-owned views, read by the reporting role; it is not a private store for the Reporting module. Notifications already runs as a separate service with its own store.

Relational databases have supported this for a long time. The PostgreSQL 18 documentation on schemas (checked on 5 November 2026) lists, among the reasons to use schemas, "to allow many users to use one database without interfering with each other", and states that "by default, users cannot access any objects in schemas they do not own". Access must be granted explicitly. Other database engines offer equivalent features. The mechanism is ordinary. Using it as an architecture boundary is the decision.

Four ownership rules

After the report incident, the Supplier Desk made its ownership rules complete and wrote them down. The first was already in place for every module except reporting; the others had been habits rather than rules.

1. One schema per module, one role per module. Each module's data-access code connects with a role that can use only its own schema. Verification's role cannot read Cases' tables, and the reverse. A cross-module query fails at run time with a permission error, which is far cheaper than a wrong report three weeks later. The reporting stopgap role was removed once the read path below was live.

2. The owning module owns the migrations. Schema changes for Verification live in the Verification module's code and are reviewed by its owners. No other module ships a migration that touches another module's schema. The team uses one migration history per module so that the order of changes within a module is clear.

3. Other modules read through contracts or published copies. When Cases needs verification status, it calls Verification's contract or keeps a copy updated from Verification's events. It never queries Verification's tables. A copy is allowed to be a few seconds stale; the screen says when it was last updated.

4. Changes to a module's tables are private until they reach its contract. The Verification team can rename a column whenever it likes, as long as its contract and its published events stay compatible. That is what makes internal change safe.

Transactions across modules are a design smell, not a ban

Inside one database server, it is technically possible for one transaction to update tables in two schemas. That is one of the genuine advantages of a modular monolith over separate services, and the Supplier Desk does not pretend otherwise. It does treat every cross-module transaction as a decision that must be recorded.

The team found three such transactions. Two disappeared once they asked what the business actually required. Opening a case and requesting re-verification did not need to succeed or fail together: the case can be open while a re-verification request is retried. The third was real. When a supplier's registration is suspended, all of its open verification requests must be cancelled in the same instant, because a suspended supplier must not be marked verified a minute later.

That transaction stays, for now, with a note in the architecture decision record: "Cross-module transaction between Registration and Verification. If Verification is ever extracted, this becomes a SupplierSuspended event or command. Verification must then refuse to publish outcomes for suspended suppliers and revoke any it published in the gap." That is a weaker guarantee than one transaction, and the record says so. Our article on why a successful payment can still end in a failed order covers the general pattern for business actions that can no longer commit together. The record makes the future cost visible today, which is exactly what the extraction tests ask for when they ask whether a module can own its data without shared joins.

Reporting needs data from everyone and ownership of nothing

Reporting is the hardest case, because a report legitimately needs data from every module. The answer is not to give the reporting module a role that can read every schema. That simply recreates the 280-line query with more permissions.

A published read path for reportingRegistrationVerificationCasesOther modulesModule-ownedreporting views(part of contract)Reporting role:views onlyNightly copywith timestampDistrict reportstates freshnessNo access to internal tables
Each module publishes reporting views as part of its contract and keeps their shape stable while its internal tables change. The reporting role can read only those views, and the district report reads a nightly copy that states when it was refreshed, so heavy queries never touch supplier-facing work.

The Supplier Desk chose a published read path with three parts:

  • Reporting views owned by each module. Each module publishes one or more views in a dedicated reporting schema. The view is part of the module's contract. Verification can rename internal columns freely, but its reporting view keeps the same shape, or it publishes a new version with notice.
  • A reporting role that can read only those views. It has no access to any module's internal tables.
  • A refresh schedule and a stated freshness. Kagiso's monthly report reads a nightly copy of the views, not the live tables, so heavy queries never slow down supplier-facing work. The report states the time of the last refresh.

The report broke once more, during the change-over, when Verification's view was found to define "complete" differently from the old query. That was a useful failure. The definition is now written in the view's documentation, and the district managers agreed which definition the department means. Weak data ownership tends to surface exactly like this, as AI projects expose weak data ownership shows in another setting. Our article on why data quality and observability are operational disciplines covers what to monitor once that path is live.

Why this matters for extraction

Data ownership inside one database is not only tidiness. It is the cheapest possible preparation for extraction. If a module already owns its schema, writes only through its own role, publishes its data through a contract and has recorded its cross-module transactions, then moving its data to a separate database is mostly a mechanical exercise. Part 7 walks through that exercise for the Verification module.

If it does not, the first step of every extraction is an archaeological dig through queries nobody remembers writing. Our Reuse, refactor, replace or retire article describes how often that dig dominates the cost of change.

Migrations still share a release

One real cost of a single deployable unit is that every module's migrations run in the same release. If Verification's migration fails, Documents' feature waits too. The Supplier Desk reduced that coupling with two habits rather than more infrastructure.

First, every schema change follows an expand-and-contract sequence. A release adds the new column or table and writes to both old and new shapes. A later release, once nothing reads the old shape, removes it. No single release both adds and removes, so a failed deployment can be rolled back without data loss.

Second, each module's migrations are tested on a copy of production-sized synthetic data in the nightly build, so a slow migration is found before release day rather than during it. Neither habit is specific to modular monoliths. Both become more valuable when several owners share one release train, and both carry over unchanged if a module is later extracted.

When one database really is shared ownership

There are cases where shared tables are correct. A small internal tool with one domain and one owner does not need six schemas. A reference table of South African provinces and municipalities can reasonably be read by everyone. The test is whether two modules write to the same data, or interpret it differently. If they do, ownership must be decided. If they do not, a shared read-only reference table is fine, as long as one module owns its changes.

A data ownership checklist

QuestionSupplier Desk answer
Which module owns each table?Recorded in each module's manifest; no table without an owner
Can a module query another module's tables?No; enforced by database roles
Who reviews migrations to a schema?The owning module's owners only
Which transactions cross modules, and why?One, recorded in a decision record with its extraction consequence
How do other modules read the data?Contract calls, event-fed copies, or published reporting views
What freshness does each copy promise?Stated on screen or in the report
Who decides a shared definition, such as "verified"?The owning module, agreed with the business owner

Key points

  • One database server can hold several owned schemas. Physical sharing does not require shared ownership.
  • Give each module one schema, one role and its own migrations, and make cross-module queries fail by design.
  • Treat each cross-module transaction as a recorded decision with a known cost if the module is ever extracted.
  • Give reporting a published read path of module-owned views, a read-only role and a stated freshness.
  • Data ownership inside the monolith is the cheapest preparation for any later extraction.

Previous: Part 3, Local calls became network calls. Now what?. Next: Part 5, Microservices are an organisational architecture. All parts are on the series page.

How CloudNala can help

CloudNala helps teams turn one shared database into clearly owned data. We map which modules read and write which tables, design schemas, roles and migration ownership per module, record the cross-module transactions that remain, and build a reporting read path that serves the business without reopening every table, so that your data boundaries are as real as your code boundaries.


Work with CloudNala

CloudNala helps organisations move from technology ambition to practical execution across cloud, AI, data, platform engineering and digital services.

Whether you are exploring AI, modernising your cloud environment, building a public-sector digital service, or turning an idea into a working MVP, we can help you shape the roadmap and deliver the next step.

Book an Architecture Complexity Review or write to us at consult@cloudnala.co.za