Spanner, Firestore and Bigtable

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Commands and behaviour below are current for the gcloud CLI 586 line, the Google Cloud Client Libraries for Java (through the libraries-bom) and Spring Cloud GCP 8.x (Spring Boot 4) or 7.4.x (Spring Boot 3), checked 2026-10-01 against the Spanner, Firestore and Bigtable documentation. Spanner editions went generally available on 2024-09-24, Firestore gained editions and MongoDB compatibility (GA 2025-08-26) and Bigtable gained GoogleSQL support, so confirm flags with gcloud spanner instances create --help, gcloud firestore databases create --help and gcloud bigtable instances create --help before relying on them in automation. The project my-project-id and the region us-central1 are placeholders.

Google Cloud has three databases that exist only on Google Cloud and that scale horizontally without you running servers: Spanner, a relational database with strongly consistent transactions that can span regions; Firestore, a serverless document database with real-time listeners and direct access from mobile and web clients; and Bigtable, a wide-column store for very high throughput on sorted keys. They are different answers to different questions, so this page describes each, then ends with a comparison and a rule of thumb for choosing. In the Bookshelf scenario the relational catalog lives in Cloud SQL (see Cloud SQL) and the orders data is a document workload (see MongoDB on Google Cloud for the MongoDB-compatible variant). This page uses the same Bookshelf domain to show where each of the three would fit: Spanner if orders must be relational and globally consistent, Firestore for per-user data read straight from the browser, and Bigtable for a high-volume price-history or click stream. The map of every Google Cloud database is in Managed Databases on Google Cloud; the engine-neutral guidance on when a key-value, document or relational model fits is in Choosing the Right Database. The AWS and Azure counterparts are Amazon DynamoDB and Cosmos DB and Azure DocumentDB.

The same Bookshelf order as a Spanner relational table, a Firestore document and a Bigtable wide-column row, with the strengths and typical use of each

Choosing between them

Decide on the access pattern and the consistency you need, not on size alone.

Spanner Firestore Bigtable Cloud SQL / AlloyDB

Data model

Relational tables, SQL (GoogleSQL or PostgreSQL dialect), also graph, full-text and vector search.

Collections of documents; Native mode, Datastore mode or MongoDB compatibility.

Wide-column: rows sorted by one row key, column families, cells with timestamps.

Relational (PostgreSQL, MySQL, SQL Server) with full engine features.

Transactions

Strongly consistent read-write transactions across rows, tables and regions.

ACID transactions across documents in one database.

Atomic per row only; no multi-row transactions.

Full ACID within one primary.

Scale model

Horizontal writes and reads; add processing units or nodes, or let the autoscaler do it.

Serverless; scales automatically with limits on write rate to a single document and index range.

Horizontal; add nodes per cluster, scales to millions of operations per second.

Vertical for writes (bigger primary), read replicas for reads.

Availability

Regional 99.99 %, multi-region 99.999 % (Enterprise Plus for multi-region).

Regional and multi-region locations with a documented availability SLA.

Single-cluster or multi-cluster replication; availability grows with replication.

High availability configuration within a region.

Client access

Servers, through drivers and client libraries.

Servers and, with security rules, mobile and web clients directly.

Servers only (client libraries, HBase client, cbt).

Servers, through drivers or the Auth Proxy.

Typical fit

Global ledgers, inventory, order management, any relational workload that outgrew one primary.

User profiles, carts, chat, app back ends, real-time dashboards.

Time series, IoT telemetry, ad tech, personalisation, monitoring data.

Most line-of-business applications up to a single primary’s capacity.

flowchart TD A[New workload] --> B{Relational model and SQL joins needed?} B -- yes --> C{Outgrow one primary or need multi-region writes?} C -- yes --> S[Spanner] C -- no --> Q[Cloud SQL or AlloyDB] B -- no --> D{Mobile or web clients read directly or need real-time sync?} D -- yes --> F[Firestore Native mode] D -- no --> E{Sustained huge write rate on sorted keys, time series or IoT?} E -- yes --> T[Bigtable] E -- no --> F2[Firestore, or MongoDB compatibility for existing MongoDB code]

Derived from the Spanner overview, Firestore overview and Bigtable overview. Older guides present these services mainly as "NoSQL options"; Spanner is relational and consistent, and current guidance treats Firestore and Bigtable as complementary rather than interchangeable.

Spanner

Spanner is a fully managed relational database that shards data automatically into splits, replicates each split with a Paxos-based protocol and uses synchronised clocks (TrueTime) to give external consistency: transactions behave as if they ran one at a time in real-time order, even across continents. You get SQL, schemas, secondary indexes and foreign keys, and you scale by adding compute rather than by sharding in the application. It is the right tool when a relational dataset or write rate no longer fits one primary, or when you need consistent multi-region writes; it is more than you need for a small catalog, where Cloud SQL costs far less.

Instances, editions and configurations

A Spanner instance is an allocation of compute and replication placement; it contains one or more databases. Three choices define an instance:

  • Edition: Standard, Enterprise or Enterprise Plus. Editions went GA on 2024-09-24. Standard covers core relational features; Enterprise adds Spanner Graph, full-text search, vector search, the managed autoscaler and more advanced features; Enterprise Plus adds multi-region configurations, the highest availability SLA and advanced security and recovery options. Check the editions page for the exact feature matrix before choosing.

  • Instance configuration: regional (for example regional-us-central1, three read-write replicas in three zones), dual-region or multi-region (for example nam3, nam-eur-asia1), where replicas, a witness and read-only replicas are spread across regions. Placement is chosen at creation and affects latency, availability and price.

  • Compute capacity, in processing units (PUs) or nodes: 1,000 PUs equal one node. You can allocate in multiples of 100 PUs up to 1,000 and in multiples of 1,000 above that, so --processing-units=100 is one tenth of a node. A node serves a documented amount of data and throughput; the storage limit per node and the recommended CPU utilisation (65 % high-priority for regional, lower for multi-region) are in the documentation.

Small PU counts make Spanner usable for development and small production systems, and a 90-day free trial instance lets you try it without a charge (one per project, limited to 10 GB of storage, with a Standard-like feature set; see the free trial page for the current terms). Two dialects exist per database, chosen at creation and fixed afterwards: GoogleSQL and the PostgreSQL interface, which lets PostgreSQL drivers, syntax and many tools work against Spanner. They are the same engine; the dialect only changes how you write schema and queries.

Capacity: processing units and the managed autoscaler

You pay for allocated compute whether it is busy or not, so size to the workload. There are two ways:

  • Manual: set --processing-units (or --nodes) and change it with gcloud spanner instances update. Scaling is online and takes minutes.

  • Managed autoscaler (Enterprise or higher): give a minimum and a maximum in PUs or nodes and CPU and storage utilisation targets; Spanner scales in and out for you. A minimum at least covers the steady state; a maximum is your cost ceiling.

Watch the high-priority CPU utilisation, the storage utilisation and the lock wait time in Cloud Monitoring (see Monitoring and Observability); sustained CPU above the recommended target is the signal to add capacity, and a low CPU with high latency usually points at schema or hotspot problems, not capacity.

In the Google Cloud console

  1. Select the project in the project picker (or open Cloud Shell, which has gcloud ready).

  2. Open Spanner from the navigation menu; the first visit offers to enable the Cloud Spanner API.

  3. Choose Create a provisioned instance (or Start free trial for the 90-day instance).

  4. Enter an Instance name (bookshelf-spanner) and an Instance ID, choose the Edition (Enterprise), then Configuration (Regional, us-central1).

  5. Under Allocate compute capacity pick Manual allocation with 100 processing units, or Autoscaling with a minimum and a maximum.

  6. Choose Create, then in the instance page choose Create database, give it the name orders, pick the Dialect and paste the DDL from the next section.

With the gcloud CLI

export PROJECT_ID=$(gcloud config get-value project)
gcloud services enable spanner.googleapis.com

# A small regional Enterprise instance: 100 processing units is one tenth of a node
gcloud spanner instances create bookshelf-spanner \
  --config=regional-us-central1 \
  --description="Bookshelf orders" \
  --edition=ENTERPRISE \
  --processing-units=100

# Or let the managed autoscaler decide (Enterprise or higher)
gcloud spanner instances create bookshelf-spanner-auto \
  --config=regional-us-central1 \
  --description="Bookshelf orders (autoscaled)" \
  --edition=ENTERPRISE \
  --autoscaling-min-processing-units=100 \
  --autoscaling-max-processing-units=2000 \
  --autoscaling-high-priority-cpu-target=65 \
  --autoscaling-storage-target=90

# Change capacity manually later
gcloud spanner instances update bookshelf-spanner --processing-units=500

gcloud spanner instances list --format="table(name,config,edition,processingUnits)"

Schema design: keys, splits and interleaving

Spanner stores every table sorted by its primary key and divides the key space into splits that move between servers to balance load. That has two consequences for design.

Avoid hotspots. A key that always increases (a timestamp, an auto-incremented number) sends every new row to the last split, and one server becomes the bottleneck whatever the instance size. Use a UUID v4 or another high-cardinality random value, a bit-reversed sequence (Spanner’s bit_reversed_positive sequences), or lead with a well-distributed column such as the customer and put the timestamp second. Never use a monotonically increasing value as the first key part of a write-heavy table. Indexes follow the same rule: an index on a timestamp column has the same hotspot as a timestamp key.

Spanner splits and interleaving: orders physically stored next to their customer, splits placed on compute, and a hotspot caused by a monotonically increasing key

Interleave parent and child tables. Spanner can store the rows of a child table physically inside their parent row. Interleaving means that reading a customer with its orders touches one split, and that writes to both in one transaction are cheap. There are two forms, and older guides only show the first:

  • INTERLEAVE IN PARENT parent couples the lifetime of child and parent: the child’s primary key must start with the parent’s key, the parent row must exist, and ON DELETE CASCADE or ON DELETE NO ACTION decides what happens when the parent is deleted.

  • INTERLEAVE IN parent co-locates the data only: the child still has to begin with the parent’s key columns, but there is no parent-must-exist rule and no cascade, so a child row can exist without a parent row.

Use interleaving for tightly owned data (an order and its lines); use a plain foreign key for loosely related data. A very large parent with millions of interleaved children also forms a large key range, so interleave only what is read together. Secondary indexes can be interleaved in a table for the same locality reasons. The schema below is the Bookshelf order model in GoogleSQL.

CREATE TABLE Customers (
  CustomerId STRING(36) NOT NULL,
  Email      STRING(256) NOT NULL,
  CreatedAt  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp = true)
) PRIMARY KEY (CustomerId);

CREATE TABLE Orders (
  CustomerId STRING(36) NOT NULL,
  OrderId    STRING(36) NOT NULL,
  Status     STRING(16) NOT NULL,
  Total      NUMERIC NOT NULL,
  CreatedAt  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp = true)
) PRIMARY KEY (CustomerId, OrderId),
  INTERLEAVE IN PARENT Customers ON DELETE CASCADE;

CREATE TABLE OrderLines (
  CustomerId STRING(36) NOT NULL,
  OrderId    STRING(36) NOT NULL,
  LineNo     INT64 NOT NULL,
  Isbn       STRING(13) NOT NULL,
  Quantity   INT64 NOT NULL
) PRIMARY KEY (CustomerId, OrderId, LineNo),
  INTERLEAVE IN PARENT Orders ON DELETE CASCADE;

CREATE INDEX OrdersByStatus ON Orders (Status, CreatedAt DESC);

Create the database with the DDL in one call (the statements are separate elements of the --ddl list, or a file passed with --ddl-file):

gcloud spanner databases create orders \
  --instance=bookshelf-spanner \
  --database-dialect=GOOGLE_STANDARD_SQL \
  --ddl-file=schema.sql

# Schema changes later run online
gcloud spanner databases ddl update orders --instance=bookshelf-spanner \
  --ddl="ALTER TABLE Orders ADD COLUMN Notes STRING(MAX)"

gcloud spanner databases ddl describe orders --instance=bookshelf-spanner

Derived from Schema and data model, Schema design best practices and Data definition language. In the PostgreSQL dialect the same ideas apply with INTERLEAVE IN PARENT in the CREATE TABLE statement and PostgreSQL types.

Querying and transactions

Spanner reads are strong by default and can be stale on request (a timestamp bound or exact staleness), which lets a replica answer locally and is the cheapest way to serve read-heavy pages that tolerate a few seconds of lag. Writes are grouped in read-write transactions (locks, retried automatically by the client libraries when aborted) or sent as mutations, a batch of inserts and updates applied atomically, which is more efficient than SQL for bulk changes. Partitioned DML runs large updates and deletes outside a single transaction, and change streams publish row changes to Dataflow or Pub/Sub for downstream consumers. Use SELECT …​ FROM Orders@{FORCE_INDEX=OrdersByStatus} only when the optimiser picks the wrong index; the query plan in the console shows what it picked.

Enterprise and higher editions add features that remove the need for a separate store for some workloads: Spanner Graph (property graphs queried with GQL, on the same tables), full-text search (search indexes with tokenisation and ranking) and vector search (approximate and exact nearest-neighbour search over embeddings stored in a column). For Bookshelf, a vector column on the catalog would give "books similar to this one"; the comparison with other vector stores is in Comparing Vector Databases. These features are newer than most guides and some are still Preview or limited to certain editions; check the release notes for the current launch stage of each.

Access control

Spanner uses IAM at the project, instance and database level, and optional fine-grained access control with database roles inside a database. Common predefined roles:

Role Allows

roles/spanner.admin

Everything on instances and databases, including IAM; keep it for administrators.

roles/spanner.databaseAdmin

Create and drop databases, change schema, grant database roles.

roles/spanner.databaseUser

Read and write data and run queries in databases; the role for an application service account.

roles/spanner.databaseReader

Read data and run read-only queries.

roles/spanner.viewer

View instances and databases but not the data.

roles/spanner.fineGrainedAccessUser

Use a database role that is granted through fine-grained access control.

Grant the application’s service account the narrowest role on the database rather than on the project:

gcloud spanner databases add-iam-policy-binding orders \
  --instance=bookshelf-spanner \
  --member="serviceAccount:orders-api@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/spanner.databaseUser"

Derived from Spanner IAM and Fine-grained access control. Data is encrypted at rest with Google-managed keys by default, with customer-managed keys (CMEK) available per database (see Secret Manager and Cloud KMS). Reach Spanner from private networks over its normal Google API endpoints; private connectivity options are described in VPC Networking on Google Cloud.

Java: the client library, JDBC and Spring

Applications authenticate with Application Default Credentials, using the service account attached to Cloud Run or the Workload Identity Federation for GKE identity; locally use gcloud auth application-default login or service account impersonation, and never a downloaded key. The Java client library gives the fullest control:

package com.example.bookshelf.orders;

import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.Statement;
import java.util.List;

public class OrderRepository implements AutoCloseable {
    private final Spanner spanner = SpannerOptions.newBuilder().build().getService();
    private final DatabaseClient client;

    public OrderRepository(String projectId) {
        this.client = spanner.getDatabaseClient(
                DatabaseId.of(projectId, "bookshelf-spanner", "orders"));
    }

    public void createOrder(String customerId, String orderId, String status, String total) {
        client.write(List.of(
                Mutation.newInsertBuilder("Orders")
                        .set("CustomerId").to(customerId)
                        .set("OrderId").to(orderId)
                        .set("Status").to(status)
                        .set("Total").to(new java.math.BigDecimal(total))
                        .set("CreatedAt").to(com.google.cloud.spanner.Value.COMMIT_TIMESTAMP)
                        .build()));
    }

    public int countOrders(String customerId) {
        Statement statement = Statement.newBuilder(
                        "SELECT COUNT(*) FROM Orders WHERE CustomerId = @customer")
                .bind("customer").to(customerId)
                .build();
        try (ResultSet rs = client.singleUse().executeQuery(statement)) {
            rs.next();
            return (int) rs.getLong(0);
        }
    }

    @Override
    public void close() {
        spanner.close();
    }
}

Derived from the Spanner client libraries and Java reference. Add com.google.cloud:google-cloud-spanner with the libraries-bom so versions stay aligned. Three other routes reuse existing code:

  • JDBC: the open-source Spanner JDBC driver (com.google.cloud:google-cloud-spanner-jdbc) uses URLs such as jdbc:cloudspanner:/projects/my-project-id/instances/bookshelf-spanner/databases/orders, so Spring JdbcTemplate, Hibernate (with the Spanner dialect) and Flyway-style tools can run on it. Spanner is not a drop-in PostgreSQL: check the supported SQL and the unsupported features before porting.

  • Spring Data Spanner: the Spring Cloud GCP starter spring-cloud-gcp-starter-data-spanner offers repositories and SpannerTemplate, driven by spring.cloud.gcp.spanner.instance-id and spring.cloud.gcp.spanner.database. The reference lives in the Spring Cloud GCP reference; generic Spring Boot configuration is in Configuration and Profiles.

  • PGAdapter: a proxy that lets PostgreSQL clients (psql, PostgreSQL JDBC) talk to a PostgreSQL-dialect database.

For local development and tests, run the emulator: gcloud emulators spanner start (or the container image) and set SPANNER_EMULATOR_HOST=localhost:9010; the client libraries then skip authentication. The emulator does not reproduce performance or every feature, so keep a pre-production test on a real small instance.

Cost and quotas

Spanner is billed mainly on compute capacity (processing units or nodes, per hour, by edition and configuration), storage (the data and backups), network egress and, for some features, usage-based charges. Cost drivers to watch: an oversized instance left running, multi-region configurations (more replicas and more compute), long backup retention, and change streams or analytics reading heavily. Use the autoscaler or reduce PUs outside business hours for development instances, and delete throwaway instances. There is a free trial instance; there is no permanent free tier. Prices change, so use the pricing page and the pricing calculator rather than numbers in a guide. Limits (databases per instance, tables per database, mutations per commit, row and key size, sessions) are listed in Quotas and limits; hitting a limit is a design signal, not just a quota request.

Backups and recovery

Spanner offers point-in-time recovery (versions retained for 1 hour by default, up to 7 days with version_retention_period), backups (on demand or scheduled, retained up to the documented maximum) and, for Enterprise Plus, additional recovery options. Set the retention you need explicitly:

gcloud spanner databases ddl update orders --instance=bookshelf-spanner \
  --ddl="ALTER DATABASE orders SET OPTIONS (version_retention_period = '3d')"

gcloud spanner backups create orders-backup-1 \
  --instance=bookshelf-spanner --database=orders \
  --retention-period=14d --async

gcloud spanner databases restore --destination-database=orders-restored \
  --destination-instance=bookshelf-spanner \
  --source-instance=bookshelf-spanner --source-backup=orders-backup-1

Firestore

Firestore is a serverless document database. You do not create servers, choose a size or patch anything; you create a database in a location, add documents to collections, and pay for the reads, writes, deletes, storage and network you use. Its distinctive feature is that mobile and web apps can use it directly: the Firebase SDKs open a real-time listener, cache data offline and are protected by security rules rather than by a middle tier. Server code uses the same data through IAM.

Editions, modes and databases

Firestore has two editions, and a project can hold several databases (--database), including one named (default):

  • Standard edition: Native mode (the document model, real-time listeners, mobile and web SDKs, security rules) or Datastore mode (the older Datastore API for server applications, no real-time listeners or client SDKs).

  • Enterprise edition: Native mode with additional capabilities, or MongoDB compatibility, which accepts MongoDB drivers and the MongoDB Query Language (GA 2025-08-26) and is covered in MongoDB on Google Cloud.

The mode (Native or Datastore) is chosen when the database is created and cannot be changed afterwards; a project’s default database in Datastore mode cannot also use Native mode, which is why multiple databases are useful. Locations are regional (for example us-central1) or multi-region (nam5, eur3); they cannot be changed after creation. Older guides say a project may have only one Firestore database and name the product "Cloud Firestore" next to "Cloud Datastore"; both statements are out of date.

Do not confuse Firestore with Filestore, which is a managed NFS file share (see Block and File Storage on Google Cloud).

Data model and indexes

A document is a set of typed fields (strings, numbers, booleans, timestamps, maps, arrays, references, geo points, vectors); a collection is a container of documents; a document can contain subcollections. Documents are limited to roughly 1 MiB and field and nesting depths are limited too, so large blobs belong in Cloud Storage (Cloud Storage) with a reference stored in the document. Reads address a document by path (orders/o-1001) or query a collection; there are no joins, so data is denormalised: store what a screen needs together, for example the order lines inside the order document, and keep copies in sync from your code or with a function.

Indexing is automatic and central to cost and speed:

  • A single-field index exists for every field by default (ascending, descending and array-contains), so simple filters and sorts just work. Exempt large or write-heavy fields to save storage and write cost.

  • A query that filters or orders on several fields needs a composite index that you define. An unindexed query fails with an error that contains a link to create the missing index in the console.

  • Queries scale with the size of the result, not of the collection, because they read an index range.

In the Google Cloud console

  1. Select the project in the project picker (or use Cloud Shell).

  2. Open Firestore from the navigation menu; the first visit offers to enable the Cloud Firestore API.

  3. Choose Create database, select the Edition (Standard) and, for Standard, Firestore Native.

  4. Enter a Database ID (bookshelf-fs) and select a Location type (Region, us-central1).

  5. Under Security rules choose Open only for a throwaway test project; choose Restrictive for anything else, and write rules as described below.

  6. Choose Create database. Open Indexes to add composite indexes and Time to live to define TTL policies, and Disaster recovery for scheduled backups and point-in-time recovery.

With the gcloud CLI

export PROJECT_ID=$(gcloud config get-value project)
gcloud services enable firestore.googleapis.com

gcloud firestore databases create \
  --database=bookshelf-fs \
  --location=us-central1 \
  --edition=standard \
  --type=firestore-native \
  --enable-pitr \
  --delete-protection

gcloud firestore databases list --format="table(name,type,locationId,databaseEdition)"

# A composite index for "orders of a customer, newest first"
gcloud firestore indexes composite create \
  --database=bookshelf-fs \
  --collection-group=orders \
  --field-config=field-path=customerId,order=ascending \
  --field-config=field-path=createdAt,order=descending

Derived from Create and manage databases, Editions and the gcloud firestore databases create reference. Delete protection prevents accidental deletion until you turn it off with gcloud firestore databases update --no-delete-protection.

Time to live, PITR and backups

  • TTL policies delete documents whose timestamp field is in the past, with no read or write cost beyond the deletion, typically within 24 hours of expiry; use them for sessions, temporary carts and logs.

  • Point-in-time recovery (--enable-pitr) keeps seven days of versions so you can read or restore data from any minute in the window; it adds storage cost.

  • Scheduled backups keep daily or weekly snapshots for a retention you set (up to 14 weeks for weekly) and restore them to a new database.

gcloud firestore fields ttls update expireAt \
  --database=bookshelf-fs --collection-group=carts --enable-ttl --async

gcloud firestore backups schedules create \
  --database=bookshelf-fs --recurrence=daily --retention=7d

gcloud firestore databases restore \
  --source-backup=projects/my-project-id/locations/us-central1/backups/BACKUP_ID \
  --destination-database=bookshelf-fs-restored

Derived from Manage data retention with TTL, Schedule and manage backups and Point-in-time recovery. Replace BACKUP_ID with an identifier from gcloud firestore backups list.

Security: rules for clients, IAM for servers

Firestore has two separate access paths and it is important not to mix them up.

  • Server access (Cloud Run, GKE, Compute Engine, Cloud Run functions) goes through the Google Cloud APIs, is authorised by IAM, and bypasses security rules entirely. Grant the service account a predefined role such as roles/datastore.user (read and write documents), roles/datastore.viewer (read only) or roles/datastore.owner (full management). Roles are named datastore.* for historical reasons and apply to Firestore as well.

  • Client access from a browser or mobile app uses the Firebase SDKs, a Firebase Authentication identity and security rules that you deploy with the database. Without rules that allow a request, it is denied; the client SDK never receives broader access than the rules give.

A rule set is a file (firestore.rules) that matches document paths and states conditions. The classic pattern is "a user owns their own documents", using the authenticated user ID request.auth.uid:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Each user may read and write only the document named after their own uid
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    // Orders are readable by their owner; creation must carry the caller's uid
    match /orders/{orderId} {
      allow read: if request.auth != null && resource.data.customerId == request.auth.uid;
      allow create: if request.auth != null
                    && request.resource.data.customerId == request.auth.uid
                    && request.resource.data.total is number
                    && request.resource.data.total >= 0;
      allow update, delete: if false;   // changed only by the back end
    }

    // Everything else is denied
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

Deploy the rules with the Firebase CLI (firebase deploy --only firestore:rules) after firebase init firestore, or in the console under Firestore → Rules. The Rules Playground tests a request without deploying, and the emulator runs the rules in unit tests. Treat rules as code: review them, test them with the @firebase/rules-unit-testing package, and never leave allow read, write: if true in a project with real data. Rules are not filters: a query must be written so that every possible result satisfies the rule, otherwise it fails as a whole. Security rules apply to Native mode only; Datastore mode and MongoDB compatibility use IAM. Derived from Get started with Firestore security rules, rules conditions and the Firestore IAM reference.

Native-mode Firestore supports vector fields and k-nearest-neighbour queries over them with a vector index, so a small semantic search ("books like this one") can live next to the data. It has limits on dimensions and on the number of results; for large or specialised workloads see Comparing Vector Databases. See the vector search guide.

Java: the client library and Spring

Server code uses the Firestore client library with Application Default Credentials. The database ID is set on the options builder, because a project can hold several databases:

package com.example.bookshelf.orders;

import com.google.api.core.ApiFuture;
import com.google.cloud.firestore.DocumentSnapshot;
import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.Query;
import com.google.cloud.firestore.QueryDocumentSnapshot;
import com.google.cloud.firestore.WriteResult;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

public class OrderDocuments implements AutoCloseable {
    private final Firestore db = FirestoreOptions.getDefaultInstance().toBuilder()
            .setDatabaseId("bookshelf-fs")
            .build()
            .getService();

    public void save(String orderId, String customerId, double total) throws Exception {
        ApiFuture<WriteResult> result = db.collection("orders").document(orderId).set(Map.of(
                "customerId", customerId,
                "status", "PAID",
                "total", total,
                "createdAt", com.google.cloud.Timestamp.now()));
        result.get();
    }

    public List<Map<String, Object>> latestFor(String customerId) throws ExecutionException, InterruptedException {
        Query query = db.collection("orders")
                .whereEqualTo("customerId", customerId)
                .orderBy("createdAt", Query.Direction.DESCENDING)
                .limit(20);
        return query.get().get().getDocuments().stream()
                .map(QueryDocumentSnapshot::getData)
                .toList();
    }

    public boolean exists(String orderId) throws Exception {
        DocumentSnapshot snapshot = db.collection("orders").document(orderId).get().get();
        return snapshot.exists();
    }

    @Override
    public void close() throws Exception {
        db.close();
    }
}

Derived from the Firestore server client libraries guide. The composite index created earlier is what the latestFor query needs. With Spring, the Spring Cloud GCP Firestore starter (spring-cloud-gcp-starter-firestore) provides reactive repositories and FirestoreReactiveOperations, configured with spring.cloud.gcp.firestore.database-id; see the Spring Cloud GCP Firestore reference and, for the MongoDB-compatible alternative that works with Spring Data MongoDB, the next page in this section.

For development and tests use the Firestore emulator (gcloud emulators firestore start, then FIRESTORE_EMULATOR_HOST=localhost:8080); it also runs security rules through the Firebase Emulator Suite.

Cost and quotas

Firestore bills per document read, write and delete, storage (documents, indexes and, if enabled, PITR and backups), network egress and some feature usage. Cost drivers: queries that read many documents to show a few, unneeded indexes on every field, real-time listeners that fan out large result sets, and long PITR windows. There is a free quota per day for the (default) database (a documented number of reads, writes and deletes and some storage), which makes it cheap for prototypes; check the pricing page for the current figures. Limits to design around are the sustained write rate to a single document (about once per second), index entries per document, document size and the maximum transaction duration; see quotas and limits and pricing.

Bigtable

Bigtable is a sparse, distributed, sorted map: a table holds rows identified by a single row key, each row has column families that group columns, and each cell can hold several timestamped versions. It is the service behind many Google products and is built for enormous scale and steady low-latency reads and writes by key. It has no joins and no secondary indexes; all of the design effort is in the row key.

Instances, clusters and nodes

  • An instance is a container for clusters and tables; its replication and application profiles are managed at this level.

  • A cluster lives in one zone and has a number of nodes (compute that serves rows) and storage (SSD or HDD). A production instance usually has at least one node per cluster; development instances were retired, so use one small cluster and delete it afterwards. Add clusters in other zones or regions for availability and locality, with replication between them.

  • Autoscaling sets a minimum and maximum number of nodes and CPU and storage utilisation targets, so the cluster follows load; otherwise you set a fixed node count.

Row key design

Bigtable stores rows in sorted order by row key and splits the table into tablets that move between nodes. A good key is one you can scan by prefix and that spreads load across the whole key space. Avoid keys that start with a timestamp or a sequential number, which send all writes to one node (the same hotspot problem as in Spanner), and avoid very long keys. A common time-series pattern is entity#reversed-timestamp, for example the ISBN followed by a descending timestamp, so the newest price for a book is the first row of its prefix. Group columns that are read together in the same family and keep the number of families small; use garbage-collection policies (maximum age or maximum number of versions) to bound storage.

In the Google Cloud console

  1. Select the project and open Bigtable in the navigation menu, enabling the Cloud Bigtable Admin API if asked.

  2. Choose Create instance, enter an Instance name (bookshelf-bt) and Instance ID.

  3. Choose the Storage type (SSD for most workloads) and configure the first cluster: an ID, the Region and zone (us-central1-a), and Autoscaling or a fixed number of nodes.

  4. Choose Create, then Tables → Create table to add a table and its column families; Bigtable Studio runs GoogleSQL queries against it.

With the gcloud CLI and cbt

gcloud services enable bigtableadmin.googleapis.com bigtable.googleapis.com

gcloud bigtable instances create bookshelf-bt \
  --display-name="Bookshelf price history" \
  --cluster-config=id=bookshelf-bt-c1,zone=us-central1-a,nodes=1

# Autoscaling instead of a fixed node count
gcloud bigtable clusters update bookshelf-bt-c1 --instance=bookshelf-bt \
  --autoscaling-min-nodes=1 --autoscaling-max-nodes=3 --autoscaling-cpu-target=60

# cbt, the Bigtable command-line tool, ships as a gcloud component
gcloud components install cbt
echo "project = $PROJECT_ID" > ~/.cbtrc
echo "instance = bookshelf-bt" >> ~/.cbtrc

cbt createtable prices
cbt createfamily prices cf
cbt set prices "isbn#9780134685991#9999" cf:amount=31.00
cbt read prices prefix="isbn#9780134685991" count=5

Derived from Create an instance, Autoscaling and the cbt tool. The gcloud components install command is only available when the CLI was installed from the Google package rather than a system package manager.

GoogleSQL, clients and access

Bigtable supports GoogleSQL for Bigtable (SQL queries on tables, with the same functions as BigQuery where they apply), alongside the native ReadRows and MutateRows API. Clients are the Cloud Bigtable client libraries (Java: BigtableDataClient), the HBase client (bigtable-hbase) for applications written against Apache HBase, Dataflow, Dataproc and BigQuery external tables. There is no Spring Data module for Bigtable, so use the client library from a Spring bean. IAM roles include roles/bigtable.admin, roles/bigtable.user (read and write data), roles/bigtable.reader and roles/bigtable.viewer; give the application roles/bigtable.user on the instance. The emulator is started with gcloud emulators bigtable start.

package com.example.bookshelf.prices;

import com.google.cloud.bigtable.data.v2.BigtableDataClient;
import com.google.cloud.bigtable.data.v2.models.Query;
import com.google.cloud.bigtable.data.v2.models.Row;
import com.google.cloud.bigtable.data.v2.models.RowMutation;

public class PriceHistory {
    public void record(String projectId, String isbn, long reversedTimestamp, String amount) throws Exception {
        try (BigtableDataClient client = BigtableDataClient.create(projectId, "bookshelf-bt")) {
            client.mutateRow(RowMutation.create("prices", "isbn#" + isbn + "#" + reversedTimestamp)
                    .setCell("cf", "amount", amount));
        }
    }

    public String latest(String projectId, String isbn) throws Exception {
        try (BigtableDataClient client = BigtableDataClient.create(projectId, "bookshelf-bt")) {
            for (Row row : client.readRows(Query.create("prices").prefix("isbn#" + isbn + "#").limit(1))) {
                return row.getCells("cf", "amount").get(0).getValue().toStringUtf8();
            }
            return null;
        }
    }
}

Derived from the Bigtable client libraries. In a real application create one long-lived client and reuse it; opening one per call as above only keeps the sample short.

When Bigtable fits, and cost

Choose Bigtable for time series, IoT and telemetry, monitoring data, ad and recommendation features, and any workload with hundreds of thousands of operations per second addressed by a well-designed key, with large data sizes (terabytes and up). Do not choose it for small datasets (a single node is a fixed monthly cost regardless of use), relational queries, multi-row transactions or ad hoc filtering on columns. Billing is on node hours (per cluster), storage (SSD or HDD, plus backups) and network egress; the cheapest way to cut cost is autoscaling and removing clusters you do not need. See pricing and quotas and limits.

Troubleshooting

Symptom Likely cause and fix

Spanner ABORTED errors under load

Contention on a hot key or long transactions; the client retries automatically, but fix the key design or shorten transactions. Use the lock statistics tables to find the keys.

Spanner writes slow while CPU is low

A hotspot on a monotonically increasing key or index; use a random or bit-reversed first key part.

Firestore FAILED_PRECONDITION: The query requires an index

Open the link in the error message to create the composite index, or define it with gcloud firestore indexes composite create and wait until it is READY.

Firestore client gets PERMISSION_DENIED from the browser

The security rules deny the request; test it in the Rules Playground and check that the request carries the authenticated user.

Firestore server code gets PERMISSION_DENIED

The service account lacks a roles/datastore.* role on the project, or the call targets the wrong database ID.

Firestore slow or rejected writes to one document

Sustained writes above about one per second to one document; shard the counter across several documents.

Bigtable uneven CPU across nodes

A hotspot from sequential row keys; use the Key Visualizer to see the hot ranges and redesign the key.

Any of the three returns UNAVAILABLE or DEADLINE_EXCEEDED from a private VM

The VM lacks a route or Private Google Access to the Google APIs; see VPC Networking on Google Cloud.

Clean up

Delete in this order, and delete the throwaway project instead if you created one only for this page (gcloud projects delete my-project-id, see Resource Hierarchy and Projects).

# Spanner: delete backups, then databases, then the instance
gcloud spanner backups delete orders-backup-1 --instance=bookshelf-spanner --quiet
gcloud spanner databases delete orders --instance=bookshelf-spanner --quiet
gcloud spanner instances delete bookshelf-spanner --quiet

# Firestore: turn off delete protection first, then delete the database and its backup schedule
gcloud firestore databases update --database=bookshelf-fs --no-delete-protection
gcloud firestore databases delete --database=bookshelf-fs --quiet

# Bigtable: delete tables, then the instance (this also deletes its clusters)
cbt deletetable prices
gcloud bigtable instances delete bookshelf-bt --quiet

Derived from gcloud spanner instances delete, gcloud firestore databases delete and gcloud bigtable instances delete. A Spanner instance with delete protection or with backups cannot be deleted until those are removed; Firestore backups kept for their retention period are billed until they expire, so delete the backup schedule and the backups too. In the console use Delete on the instance or database page and type its name. Stop the emulators with Ctrl+C.