Real World Example

ShopEase for Business

ShopEase now hosts stores for many small brands (like Shopify or Dukaan). Small brands share one schema with a tenant_id; products are soft deleted so old orders and invoices stay valid; a filter lets the support team see deleted products.

@MappedSuperclass
public abstract class TenantEntity {
    @TenantId
    @Column(name = "tenant_id", nullable = false, updatable = false)
    private String tenantId;
}

@Entity
@Table(name = "products")
@SQLDelete(sql = "update products set deleted_at = current_timestamp where id = ? and version = ?")
@FilterDef(name = "hideDeleted")
@Filter(name = "hideDeleted", condition = "deleted_at is null")
public class Product extends TenantEntity {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private String name;
    private BigDecimal price;
    private Instant deletedAt;
    @Version private Long version;
}

@Aspect
@Component
@RequiredArgsConstructor
class HideDeletedAspect {
    private final EntityManager em;

    // every service call hides deleted products unless the caller is support staff
    @Before("execution(* com.shopease..*Service.*(..))")
    public void enable() {
        boolean support = SecurityContextHolder.getContext().getAuthentication()
                .getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_SUPPORT"));
        Session session = em.unwrap(Session.class);
        if (!support && session.getEnabledFilter("hideDeleted") == null) session.enableFilter("hideDeleted");
    }
}

Safety Net in the Database

A single forgotten condition in a native query can leak another brand's data. Big SaaS teams add a second wall in the database. PostgreSQL row-level security does that:

ALTER TABLE products ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON products
    USING (tenant_id = current_setting('app.tenant_id'));
-- the app runs  SET app.tenant_id = 'brand-a'  when it borrows a connection

Tip: Write an automated test that logs in as tenant A and calls every list endpoint, asserting no tenant B rows come back. Run it on every build.