Series MapLesson 33 / 60
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Build CoreOrdered learning track

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.

13 min read2520 words
PrevNext
Lesson 3360 lesson track12–33 Build Core
#persistence-layer#multi-tenancy#tenant-isolation#row-level-security+7 more

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_id menyebabkan 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 LOCAL harus 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 LOCAL dalam 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_id tidak 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:

FailureRoot CauseDetectionPrevention
Cross-tenant readMissing tenant predicatesecurity incident, test failmandatory tenant scope in repository
Cross-tenant updateUPDATE by id onlyunexpected data mutationtenant in WHERE + FK constraint
Cache leakagecache key missing tenantuser sees wrong datatenant-aware cache key
Admin query reusedgeneric search methodaudit anomalyseparate admin repository
RLS context leakconnection session not resetdata from previous tenantSET LOCAL + transaction discipline
Index not tenant-awaretable scan per tenantslow querycomposite index with tenant/access pattern
MyBatis/JPA inconsistencydifferent tenant filtersstale/leaked datashared scope contract + tests
Event missing tenantconsumer processes wrong scopeinbox mismatchtenant metadata required

19. Debugging Cross-Tenant Bug

Jika ada indikasi data tenant salah muncul:

  1. Identifikasi request/event entry point.
  2. Ambil tenant resolved dari auth/header/event metadata.
  3. Trace application service call.
  4. Temukan repository/mapper/entity query.
  5. Periksa SQL aktual.
  6. Pastikan tenant_id ada di predicate.
  7. Cek cache key dan cache hit.
  8. Cek JPA filter/RLS state.
  9. Cek transaction dan connection reuse.
  10. Cek data integrity FK tenant.
  11. Cek admin/support query yang mungkin mengubah/read data.
  12. 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_id di WHERE?
  • Apakah optimistic lock juga tenant-scoped?
  • Apakah bulk operation membatasi tenant?
  • Apakah migration/data fix script tenant-aware?

Schema Integrity

  • Apakah table punya tenant_id jika 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.

Lesson Recap

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.