Tenant ID, Schema/Database Strategy, Tenant Filters, RLS, Indexing, and Cache Isolation
Persistence Layer Part 033 — Multi-Tenancy in Persistence Layer
Tenant ID, shared database shared schema, separate schema, separate database, tenant discriminator, tenant filter, row-level security, JPA multi-tenancy, MyBatis tenant condition, accidental cross-tenant query, tenant-aware index, tenant-aware cache, dan multi-tenancy review checklist.
Part 033 — Multi-Tenancy in Persistence Layer
Multi-tenancy adalah salah satu area persistence layer yang paling berbahaya karena bug-nya sering bukan sekadar data salah, tetapi data tenant lain bocor.
Dalam sistem enterprise seperti CPQ, quote management, order management, catalog, pricing, billing, dan telco BSS/OSS, tenant bisa berarti banyak hal:
- customer/account besar
- business unit
- operator/telco partner
- region
- organization
- reseller
- enterprise customer
- logical partition dalam satu deployment
- deployment-specific customer pada on-prem atau hybrid cloud
Istilah internal harus diverifikasi. Jangan mengasumsikan bahwa tenant_id, account_id, org_id, customer_id, company_id, atau partition_id punya semantics yang sama.
Core principle:
Tenant isolation harus ditegakkan sebagai invariant persistence layer, bukan hanya sebagai filter UI atau authorization di controller.
Jika query tidak membawa tenant boundary dengan eksplisit, query itu harus dicurigai.
1. Apa Itu Multi-Tenancy di Persistence Layer?
Multi-tenancy di persistence layer adalah desain penyimpanan data agar banyak tenant dapat berbagi sistem, tetapi tetap terisolasi secara:
- data visibility
- mutation permission
- query scope
- cache scope
- index access pattern
- audit trail
- backup/restore boundary
- operational troubleshooting
- migration impact
Contoh tabel shared-schema:
CREATE TABLE quote (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
quote_number TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
Query aman:
SELECT *
FROM quote
WHERE tenant_id = :tenantId
AND id = :quoteId;
Query berbahaya:
SELECT *
FROM quote
WHERE id = :quoteId;
Walaupun id global UUID, query kedua tetap berbahaya jika authorization dan data ownership tenant harus selalu diverifikasi.
2. Tenant Boundary Bukan Sama dengan User Authorization
User authorization menjawab:
Can this user perform this action?
Tenant isolation menjawab:
Can this request see or mutate this tenant's data?
Keduanya harus konsisten, tetapi tidak identik.
Contoh request lifecycle JAX-RS:
HTTP request
-> authentication filter
-> tenant resolution
-> authorization policy
-> application service
-> transaction boundary
-> repository/mapper/entity manager
-> PostgreSQL query with tenant predicate
Jika tenant resolution hanya dilakukan di controller tetapi tidak diteruskan ke repository, persistence layer menjadi lemah.
Bad smell:
quoteRepository.findById(quoteId);
Better:
quoteRepository.findByTenantIdAndId(tenantId, quoteId);
Atau gunakan typed scope object:
record TenantScope(UUID tenantId) {}
quoteRepository.findById(scope, quoteId);
Keuntungan TenantScope:
- method signature lebih eksplisit
- sulit lupa tenant
- mudah diperluas dengan region/org/security scope
- bisa divalidasi di boundary
3. Model Multi-Tenancy Utama
Ada beberapa model umum.
3.1 Shared Database, Shared Schema
Semua tenant ada dalam database dan schema yang sama, dibedakan oleh tenant_id.
postgres database
public.quote
public.order
public.product_catalog
Keunggulan:
- operationally simple
- migration satu kali
- connection pool sederhana
- query lintas tenant untuk admin/reporting lebih mudah
- cost lebih rendah
Risiko:
- query lupa
tenant_idmenyebabkan leakage - index harus tenant-aware
- noisy neighbor lebih mungkin
- restore per tenant lebih sulit
- row count besar pada table shared
Cocok jika:
- tenant banyak
- tenant relatif kecil/menengah
- isolation requirement tidak membutuhkan physical separation
- sistem punya discipline query kuat
3.2 Shared Database, Separate Schema
Satu database, banyak schema.
postgres database
tenant_a.quote
tenant_b.quote
tenant_c.quote
Keunggulan:
- isolation lebih kuat dari shared schema
- restore/migration per schema lebih mungkin
- tenant-specific customization lebih fleksibel
Risiko:
- migration lebih kompleks
- connection/session harus set schema dengan benar
- jumlah schema banyak bisa menyulitkan operasional
- query dynamic schema rawan injection jika tidak dikontrol
3.3 Separate Database per Tenant
Setiap tenant punya database sendiri.
tenant_a_db.quote
tenant_b_db.quote
tenant_c_db.quote
Keunggulan:
- isolation kuat
- backup/restore per tenant lebih mudah
- noisy neighbor lebih mudah dikontrol
- compliance tertentu lebih mudah
Risiko:
- migration orchestration berat
- connection pool per tenant dapat meledak
- operational overhead tinggi
- cross-tenant reporting lebih sulit
- onboarding tenant lebih kompleks
3.4 Separate Deployment per Tenant
Setiap tenant punya deployment/service/database sendiri.
tenant_a cluster/service/db
tenant_b cluster/service/db
Keunggulan:
- isolation sangat kuat
- cocok untuk on-prem, regulated customer, atau deployment khusus
- tenant-specific release/upgrade mungkin
Risiko:
- operational cost tinggi
- version drift
- observability fragmentation
- support matrix berat
Internal verification checklist:
- Model multi-tenancy mana yang dipakai?
- Apakah model berbeda antara cloud, on-prem, dan hybrid deployment?
- Apakah istilah tenant sama di semua service?
- Apakah tenant boundary berada di API, service, persistence, database, atau semua layer?
4. Tenant ID sebagai Persistence Invariant
Pada shared-schema model, tenant_id harus dianggap sebagai bagian dari identity bisnis.
Jangan berpikir:
quote identified by id
Lebih aman:
quote identified within tenant boundary by tenant_id + id
Walaupun id global unique, query tetap harus membawa tenant predicate untuk defense-in-depth.
Contoh repository signature:
Optional<QuoteEntity> findByTenantIdAndId(UUID tenantId, UUID quoteId);
int updateStatus(
UUID tenantId,
UUID quoteId,
QuoteStatus expectedStatus,
QuoteStatus newStatus
);
Contoh SQL:
UPDATE quote
SET status = :newStatus,
updated_at = now()
WHERE tenant_id = :tenantId
AND id = :quoteId
AND status = :expectedStatus;
Jika tenant_id tidak ada di WHERE, update bisa:
- mengubah row tenant lain
- mengabaikan authorization boundary
- lolos saat test single-tenant
- gagal hanya di production multi-tenant
5. MyBatis Tenant Condition
MyBatis memberi SQL visibility tinggi, tetapi juga berarti developer bertanggung jawab penuh menulis tenant predicate.
Mapper yang baik:
<select id="findQuoteById" resultMap="QuoteResultMap">
SELECT
q.id,
q.tenant_id,
q.quote_number,
q.status,
q.created_at,
q.updated_at
FROM quote q
WHERE q.tenant_id = #{tenantId}
AND q.id = #{quoteId}
</select>
Mapper yang berbahaya:
<select id="findQuoteById" resultMap="QuoteResultMap">
SELECT
q.id,
q.tenant_id,
q.quote_number,
q.status
FROM quote q
WHERE q.id = #{quoteId}
</select>
Masalah dynamic SQL:
<where>
<if test="tenantId != null">
tenant_id = #{tenantId}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
Ini berbahaya karena tenant predicate menjadi optional.
Lebih baik:
<where>
tenant_id = #{tenantId}
<if test="status != null">
AND status = #{status}
</if>
</where>
Atau pisahkan query admin lintas tenant dengan nama eksplisit:
List<QuoteSummary> adminSearchAcrossTenants(AdminQuoteSearchQuery query);
Bukan:
List<QuoteSummary> search(QuoteSearchQuery query);
Review rule:
Tenant filter untuk business query tidak boleh optional kecuali method-nya secara eksplisit adalah cross-tenant admin/reporting query.
6. JPA Tenant Filtering
Di JPA/Hibernate, tenant isolation bisa diimplementasikan melalui beberapa pendekatan:
- explicit repository method dengan
tenantId - JPQL predicate eksplisit
- Hibernate filter
- multi-tenancy support provider
- database row-level security
- combination
Explicit JPQL:
TypedQuery<QuoteEntity> query = entityManager.createQuery("""
select q
from QuoteEntity q
where q.tenantId = :tenantId
and q.id = :quoteId
""", QuoteEntity.class);
query.setParameter("tenantId", tenantId);
query.setParameter("quoteId", quoteId);
Repository method:
Optional<QuoteEntity> findByTenantIdAndId(UUID tenantId, UUID quoteId);
Hibernate filter example:
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = UUID.class))
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
@Entity
@Table(name = "quote")
class QuoteEntity {
@Id
private UUID id;
@Column(name = "tenant_id", nullable = false)
private UUID tenantId;
}
Risk dengan filter:
- filter lupa di-enable
- native query bypass filter
- MyBatis query tidak ikut filter
- background job tidak punya tenant context
- admin query perlu bypass secara eksplisit
- test tidak mendeteksi karena hanya satu tenant
JPA/Hibernate filter membantu, tetapi tidak menggantikan review query.
7. Row-Level Security di PostgreSQL
PostgreSQL Row-Level Security dapat menegakkan tenant boundary di database.
Contoh konseptual:
ALTER TABLE quote ENABLE ROW LEVEL SECURITY;
CREATE POLICY quote_tenant_policy
ON quote
USING (tenant_id = current_setting('app.tenant_id')::uuid);
Lalu aplikasi mengatur tenant context pada connection/session:
SET LOCAL app.tenant_id = '...';
Keunggulan RLS:
- defense-in-depth kuat
- query lupa tenant predicate tetap dibatasi database
- protection berlaku untuk SQL manual, MyBatis, JPA native query
Risiko:
- connection pool harus reset session state dengan benar
SET LOCALharus berada dalam transaction- debugging query lebih sulit jika context tidak jelas
- admin/batch job perlu policy khusus
- performance plan perlu diuji
- migration dan maintenance script perlu awareness
Critical warning:
Jika memakai connection pool, jangan menyimpan tenant context sebagai session state permanen tanpa reset yang benar. Tenant context bocor antar request adalah incident security serius.
Internal verification checklist:
- Apakah PostgreSQL RLS digunakan?
- Bagaimana tenant context diset pada connection?
- Apakah memakai
SET LOCALdalam transaction? - Apakah pool membersihkan session state?
- Bagaimana admin job bypass policy?
- Apakah migration user terkena RLS?
- Apakah test mencakup tenant leakage?
8. Tenant Resolution dalam JAX-RS Lifecycle
Tenant biasanya berasal dari:
- token claim
- request header
- subdomain
- path parameter
- authenticated account mapping
- organization selection
- deployment config
- message/event metadata
Contoh JAX-RS flow:
@Provider
public class TenantResolutionFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext context) {
TenantScope tenantScope = resolveTenant(context);
context.setProperty("tenantScope", tenantScope);
}
}
Kemudian application service menerima tenant scope:
public QuoteDto getQuote(TenantScope scope, UUID quoteId) {
return quoteRepository.findById(scope, quoteId)
.map(mapper::toDto)
.orElseThrow(NotFoundException::new);
}
Jangan biarkan repository mengambil tenant dari global mutable static state kecuali framework internal benar-benar menjaminnya.
Bad smell:
UUID tenantId = TenantContextHolder.getTenantId();
quoteMapper.findById(quoteId);
Risiko:
- async boundary kehilangan context
- thread reuse bocor context
- unit test menyembunyikan dependency
- Kafka consumer tidak punya HTTP context
- scheduled job salah tenant
Lebih explicit:
quoteMapper.findByTenantIdAndId(scope.tenantId(), quoteId);
9. Tenant Context dalam Kafka/RabbitMQ Consumer
HTTP request bukan satu-satunya entry point.
Event consumer juga harus membawa tenant boundary.
Event payload minimal harus punya metadata tenant jika event bersifat tenant-scoped:
{
"eventId": "...",
"tenantId": "...",
"eventType": "QuoteSubmitted",
"occurredAt": "2026-07-11T10:00:00Z",
"payload": {
"quoteId": "..."
}
}
Consumer persistence logic:
@Transactional
public void handle(QuoteSubmittedEvent event) {
TenantScope scope = new TenantScope(event.tenantId());
QuoteEntity quote = quoteRepository.findById(scope, event.quoteId())
.orElseThrow(...);
inboxRepository.markProcessed(scope, event.eventId());
}
Jika inbox table tenant-scoped:
CREATE TABLE inbox_message (
tenant_id UUID NOT NULL,
event_id UUID NOT NULL,
status TEXT NOT NULL,
received_at TIMESTAMPTZ NOT NULL,
processed_at TIMESTAMPTZ,
PRIMARY KEY (tenant_id, event_id)
);
Tanpa tenant boundary, duplicate detection bisa salah lintas tenant.
10. Tenant-Aware Index Design
Tenant predicate harus didukung index.
Query umum:
SELECT *
FROM quote
WHERE tenant_id = :tenantId
AND status = :status
ORDER BY created_at DESC
LIMIT 50;
Index yang mungkin:
CREATE INDEX idx_quote_tenant_status_created
ON quote (tenant_id, status, created_at DESC);
Untuk lookup by business key:
CREATE UNIQUE INDEX uq_quote_tenant_quote_number
ON quote (tenant_id, quote_number);
Tanpa tenant_id, uniqueness bisa salah:
CREATE UNIQUE INDEX uq_quote_number
ON quote (quote_number);
Ini membuat dua tenant tidak bisa punya quote number sama, padahal mungkin valid.
Index design harus mengikuti access pattern:
- tenant + id
- tenant + natural key
- tenant + status + date
- tenant + customer account
- tenant + external reference
- tenant + effective date
- tenant + deleted flag
Review question:
Does the index start with tenant_id for tenant-scoped queries?
Tidak selalu harus start dengan tenant_id, tetapi harus ada alasan query-plan yang jelas jika tidak.
11. Composite Primary Key vs Surrogate ID + Tenant ID
Dua opsi umum:
Option A — Global ID primary key, tenant ID indexed
CREATE TABLE quote (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
quote_number TEXT NOT NULL
);
CREATE INDEX idx_quote_tenant_id_id
ON quote (tenant_id, id);
Keunggulan:
- entity mapping lebih sederhana
- foreign key lebih sederhana
- ID global unik
Risiko:
- developer bisa lookup by id tanpa tenant
- tenant boundary bergantung discipline query
Option B — Composite key tenant_id + id
CREATE TABLE quote (
tenant_id UUID NOT NULL,
id UUID NOT NULL,
quote_number TEXT NOT NULL,
PRIMARY KEY (tenant_id, id)
);
Keunggulan:
- tenant boundary embedded di key
- FK dapat tenant-scoped
- sulit query row tanpa tenant jika FK benar
Risiko:
- JPA mapping composite key lebih kompleks
- foreign key lebih verbose
- API dan event perlu membawa tenant + id
Dalam enterprise system, pilihan ini adalah architecture decision. Jangan mengubah tanpa ADR.
12. Foreign Key dan Tenant Consistency
Jika memakai shared schema, FK juga harus mencegah cross-tenant reference.
Kurang aman:
CREATE TABLE quote_item (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
quote_id UUID NOT NULL REFERENCES quote(id)
);
Masalah: secara teoritis quote_item.tenant_id bisa berbeda dari quote.tenant_id.
Lebih kuat dengan composite FK:
CREATE TABLE quote (
tenant_id UUID NOT NULL,
id UUID NOT NULL,
PRIMARY KEY (tenant_id, id)
);
CREATE TABLE quote_item (
tenant_id UUID NOT NULL,
id UUID NOT NULL,
quote_id UUID NOT NULL,
PRIMARY KEY (tenant_id, id),
FOREIGN KEY (tenant_id, quote_id)
REFERENCES quote (tenant_id, id)
);
Keunggulan:
- database ikut menjaga tenant consistency
- bug application lebih cepat gagal
- data repair lebih sedikit
Trade-off:
- mapping lebih kompleks
- migration lebih berat
- perlu convention kuat
13. Tenant-Aware Cache
Cache tanpa tenant key adalah leakage risk.
Bad cache key:
quote:{quoteId}
Better:
tenant:{tenantId}:quote:{quoteId}
Atau structured cache key:
record QuoteCacheKey(UUID tenantId, UUID quoteId) {}
Cache risk:
- Redis key lupa tenant
- Hibernate second-level cache menyimpan entity by id global tanpa tenant semantics
- MyBatis cache tidak aware tenant jika statement/parameter tidak jelas
- query cache untuk search tidak memasukkan tenant/filter/sort/page
- admin query mengisi cache yang dibaca user query
Review question:
Can data cached for tenant A ever be returned to tenant B?
Jika jawabannya tidak bisa dibuktikan, cache design belum aman.
14. Multi-Tenancy dengan MyBatis + JPA Mixing
Mixing MyBatis dan JPA memperbesar risiko tenant inconsistency.
Contoh:
- JPA entity memakai Hibernate filter tenant
- MyBatis mapper lupa
tenant_id - JPA repository exclude soft-deleted row
- MyBatis query include deleted row
- JPA write memakai tenant context dari filter
- MyBatis update memakai parameter manual
- Redis cache key dibuat oleh JPA path tenant-aware, tetapi MyBatis path tidak
Contoh dangerous flow:
Transaction starts
JPA loads QuoteEntity with tenant filter
MyBatis updates quote_item by quote_id only
JPA flushes quote header
Transaction commits
Masalah:
- quote header tenant-scoped
- quote item update mungkin cross-tenant jika
quote_idtidak global unique atau FK lemah - JPA persistence context tidak tahu MyBatis mutation
Rule:
Jika MyBatis dan JPA menyentuh data tenant-scoped yang sama, tenant predicate harus konsisten di dua model, dan test harus membuktikan cross-tenant isolation.
15. Admin dan Cross-Tenant Query
Tidak semua query harus tenant-scoped.
Admin/reporting/support query mungkin lintas tenant.
Tetapi lintas tenant harus eksplisit di nama method, permission, audit, dan SQL.
Contoh:
List<QuoteAdminView> searchAcrossTenants(AdminQuoteSearch query);
Bukan:
List<QuoteView> search(QuoteSearch query);
Admin query harus punya:
- explicit authorization
- reason/audit trail
- filter limit
- pagination hard limit
- redaction rules
- observability
- tenant selection if possible
- no accidental reuse by normal API
SQL admin:
SELECT tenant_id, id, quote_number, status, created_at
FROM quote
WHERE created_at >= :from
AND created_at < :to
ORDER BY created_at DESC
LIMIT :limit;
Normal query:
SELECT id, quote_number, status, created_at
FROM quote
WHERE tenant_id = :tenantId
AND created_at >= :from
AND created_at < :to
ORDER BY created_at DESC
LIMIT :limit;
Jangan memakai satu method dengan optional tenantId untuk dua semantics ini.
16. Testing Tenant Isolation
Test single tenant tidak cukup.
Minimal test harus membuat dua tenant:
Tenant A:
quote id = Q1
quote_number = Q-001
Tenant B:
quote id = Q2
quote_number = Q-001
Test lookup:
assertThat(repository.findByTenantIdAndQuoteNumber(tenantA, "Q-001"))
.hasValueSatisfying(q -> assertThat(q.tenantId()).isEqualTo(tenantA));
assertThat(repository.findByTenantIdAndQuoteNumber(tenantB, "Q-001"))
.hasValueSatisfying(q -> assertThat(q.tenantId()).isEqualTo(tenantB));
Negative test:
assertThat(repository.findByTenantIdAndId(tenantA, quoteOwnedByTenantB))
.isEmpty();
Mutation test:
int updated = repository.updateStatus(
tenantA,
quoteOwnedByTenantB,
SUBMITTED,
APPROVED
);
assertThat(updated).isZero();
Cache test:
1. Load quote for tenant A into cache.
2. Request same quote id/natural key under tenant B.
3. Assert tenant B never receives tenant A data.
17. Observability untuk Multi-Tenancy
Persistence observability harus bisa menjawab:
- tenant mana yang menghasilkan traffic tinggi?
- tenant mana yang mengalami slow query?
- tenant mana yang sering kena lock wait?
- tenant mana yang menyebabkan pool pressure?
- apakah ada query lintas tenant?
- apakah admin query dipakai secara normal?
- apakah tenant predicate hilang dari query kritikal?
Hati-hati: tenant ID bisa dianggap sensitive tergantung sistem.
Metric label tenant_id bisa menyebabkan high cardinality.
Alternatif:
- sample tenant ID hanya untuk debug mode
- hash tenant ID
- group tenant tier/segment
- use trace attribute dengan redaction policy
- log tenant hanya di secure audit log
Review question:
Can we debug tenant-specific issue without leaking tenant data in logs?
18. Failure Modes
Multi-tenancy failure modes:
| Failure | Root Cause | Detection | Prevention |
|---|---|---|---|
| Cross-tenant read | Missing tenant predicate | security incident, test fail | mandatory tenant scope in repository |
| Cross-tenant update | UPDATE by id only | unexpected data mutation | tenant in WHERE + FK constraint |
| Cache leakage | cache key missing tenant | user sees wrong data | tenant-aware cache key |
| Admin query reused | generic search method | audit anomaly | separate admin repository |
| RLS context leak | connection session not reset | data from previous tenant | SET LOCAL + transaction discipline |
| Index not tenant-aware | table scan per tenant | slow query | composite index with tenant/access pattern |
| MyBatis/JPA inconsistency | different tenant filters | stale/leaked data | shared scope contract + tests |
| Event missing tenant | consumer processes wrong scope | inbox mismatch | tenant metadata required |
19. Debugging Cross-Tenant Bug
Jika ada indikasi data tenant salah muncul:
- Identifikasi request/event entry point.
- Ambil tenant resolved dari auth/header/event metadata.
- Trace application service call.
- Temukan repository/mapper/entity query.
- Periksa SQL aktual.
- Pastikan
tenant_idada di predicate. - Cek cache key dan cache hit.
- Cek JPA filter/RLS state.
- Cek transaction dan connection reuse.
- Cek data integrity FK tenant.
- Cek admin/support query yang mungkin mengubah/read data.
- Cek logs dengan redaction policy.
Pertanyaan utama:
At what layer did tenant boundary disappear?
20. PR Review Checklist
Saat review persistence change pada multi-tenant system, tanyakan:
Query Scope
- Apakah setiap business query membawa tenant predicate?
- Apakah tenant predicate mandatory, bukan optional?
- Apakah admin/cross-tenant query dinamai eksplisit?
- Apakah count/export/search query juga tenant-scoped?
Mutation Safety
- Apakah UPDATE/DELETE memakai
tenant_iddiWHERE? - Apakah optimistic lock juga tenant-scoped?
- Apakah bulk operation membatasi tenant?
- Apakah migration/data fix script tenant-aware?
Schema Integrity
- Apakah table punya
tenant_idjika shared schema? - Apakah unique constraint memasukkan tenant jika uniqueness tenant-scoped?
- Apakah FK mencegah cross-tenant reference?
- Apakah index mendukung tenant access pattern?
MyBatis/JPA
- Apakah MyBatis mapper menulis tenant condition eksplisit?
- Apakah JPA query/filter konsisten?
- Apakah native query bypass tenant filter?
- Apakah MyBatis dan JPA menyentuh table sama dengan tenant semantics sama?
Cache
- Apakah cache key memasukkan tenant?
- Apakah query cache memasukkan tenant/filter/sort/page?
- Apakah invalidation tenant-scoped?
Testing
- Apakah test memakai minimal dua tenant?
- Apakah ada negative cross-tenant read test?
- Apakah ada negative cross-tenant update test?
- Apakah cache leakage diuji?
Observability/Security
- Apakah logs tidak membocorkan sensitive tenant data?
- Apakah audit trail mencatat tenant?
- Apakah dashboard bisa debug tenant-specific issue?
21. Internal Verification Checklist
Gunakan checklist ini di codebase/team:
- Apa definisi internal
tenant? - Apakah istilah tenant sama dengan account/org/customer/company?
- Model multi-tenancy apa yang dipakai: shared schema, separate schema, separate DB, atau separate deployment?
- Di mana tenant resolution dilakukan untuk HTTP request?
- Bagaimana tenant resolution dilakukan untuk Kafka/RabbitMQ consumer?
- Apakah repository method wajib menerima tenant scope?
- Apakah ada query by id tanpa tenant?
- Apakah MyBatis dynamic SQL membuat tenant optional?
- Apakah JPA/Hibernate filter digunakan?
- Apakah native query bypass filter?
- Apakah PostgreSQL RLS digunakan?
- Apakah connection pool aman dari tenant session leakage?
- Apakah cache key tenant-aware?
- Apakah unique index tenant-aware?
- Apakah FK menjaga tenant consistency?
- Apakah integration test selalu membuat lebih dari satu tenant?
- Apakah admin/support cross-tenant query punya audit trail?
- Apakah migration/data repair script tenant-aware?
- Apakah dashboard/log bisa debug per tenant tanpa privacy leak?
22. Senior Engineer Mental Model
Multi-tenancy bukan fitur tambahan.
Multi-tenancy adalah invariant horizontal yang memotong:
- authentication
- authorization
- API contract
- service boundary
- transaction boundary
- repository design
- mapper SQL
- JPA filter
- PostgreSQL schema
- index design
- cache key
- event metadata
- audit trail
- observability
- migration script
- testing strategy
Cara berpikir senior:
Every persistence access must prove its tenant boundary.
Jika tenant boundary hanya implicit, tersembunyi di thread-local, optional parameter, atau convention informal, maka sistem bergantung pada keberuntungan dan discipline manual.
Dalam production enterprise system, itu tidak cukup.
You just completed lesson 33 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.