Database / Google Firestore Interview questions
Last updated
1. What is Google Firestore?
Google Firestore is a fully managed, serverless NoSQL document database offered through Firebase and Google Cloud. You store data as documents grouped into collections, and each document holds fields instead of fixed table columns.
It scales automatically, so there are no servers or shards to size. Clients on web, Android, iOS, and backend languages talk to it through SDKs.
Beyond plain storage, Firestore gives you indexed queries, real-time listeners, offline caching on client SDKs, and ACID transactions. That mix is why it is popular for mobile apps and event-driven backends.
Take quiz
Rows inside fixed-schema tables
One big JSON tree under a single root
Flat key-value pairs with no nesting
Documents grouped into collections
You, by adding shards manually
Google, because the service is serverless and scales automatically
Your application, through a connection pool
You, by picking an instance size up front
2. What are the key features of Firestore?
Firestore combines a flexible document model with query power and live data sync. The features interviewers usually expect:
- Document model with nested maps and arrays, no fixed schema
- Indexed queries where speed depends on result size, not collection size
- Real-time listeners that push changes to clients
- Offline support through a local cache on mobile and web SDKs
- ACID transactions and batched writes
- Strong consistency and automatic multi-zone or multi-region replication
- Security rules plus Firebase Authentication for client access control
It also plugs into Cloud Functions, so a document write can trigger backend code.
Take quiz
Real-time listeners using onSnapshot
Composite indexes
Scheduled exports to Cloud Storage
TTL policies
Eventual consistency only
Isolation only within one collection
ACID properties
At-most-once delivery
3. What is a document in Firestore?
A document is the basic unit of storage in Firestore. It is a set of key-value fields, identified by a unique ID inside its collection, and it can be at most 1 MiB in size.
Field values can be strings, numbers, booleans, timestamps, arrays, or nested maps. A document cannot contain another document as a field, but it can have subcollections hanging off it.
users/alice { "name": "Alice", "age": 29, "address": { "city": "Pune", "zip": "411001" }, "tags": ["admin", "beta"] }
Take quiz
There is no size limit
1 MiB
10 MiB
16 KB
Only up to three levels deep
Only in Datastore mode
Yes, documents nest inside documents as fields
No, it can hold maps, and it can have subcollections
4. What is a collection in Firestore?
A collection is a container for documents. It has no schema, so two documents in the same collection can have completely different fields.
You never create a collection explicitly. It appears the moment you write the first document into it and disappears when the last document is deleted.
Collections can only contain documents, never other collections or raw values. Paths therefore alternate: collection/document/collection/document.
Because collections are schema-less, checks on field names and types live in your app code or in security rules, not in the database. A document ID only has to be unique within its own collection, so the same ID can exist in two different collections.
Take quiz
With a CREATE COLLECTION statement
By enabling it in the security rules
Implicitly, by writing the first document into it
By defining its schema in the console first
The collection no longer exists
It turns into a subcollection of the root
It is archived for later restore
It stays behind as an empty billable container
5. What is a subcollection in Firestore?
A subcollection is a collection attached to a specific document, for example users/alice/orders/o1. Nesting can go up to 100 levels deep.
It suits one-to-many data that grows without a clear limit, such as orders, messages, or comments, because it avoids the 1 MiB document cap.
Two behaviours catch people out. Fetching a parent document does not return its subcollections, and deleting a parent document does not delete them either. They have to be removed separately.
A good rule of thumb: choose a subcollection when you need to query the children on their own, and a nested map when you only ever read them together with the parent.
Take quiz
They move to a root orders collection
The delete is rejected while they exist
They are deleted automatically by cascade
Its documents remain, orphaned, until deleted separately
Only if you pass includeSubcollections
No, reads are shallow
Yes, up to three levels
Yes, but only the first 20 documents
6. What data types does Firestore support?
Firestore supports a compact set of field types:
| Type | Notes |
| String | UTF-8 text |
| Number | 64-bit integer or double |
| Boolean | true / false |
| Map | Nested object with its own fields |
| Array | Ordered list, mixed types allowed |
| Null | Explicit null value |
| Timestamp | Microsecond precision |
| GeoPoint | Latitude and longitude pair |
| Bytes | Binary data |
| Reference | Pointer to another document |
| Vector | Embedding used for nearest-neighbour search |
One restriction to remember: an array cannot directly contain another array. Wrap the inner array in a map if you need that shape.
Take quiz
Reference
Map
GeoPoint
Bytes
They are capped at 100 elements
Every element must share one type
They cannot directly contain other arrays
They can nest arrays up to five levels
7. What is a document reference in Firestore?
A DocumentReference is a lightweight object that points to a document location by its path. It is just an address, so the document may or may not exist yet.
You use it as the handle for getDoc, setDoc, updateDoc, deleteDoc, and onSnapshot. It can also be stored inside another document as a reference field.
import { doc, getDoc } from "firebase/firestore"; const ref = doc(db, "users", "alice"); // no network call yet const snap = await getDoc(ref); // billed read happens here
Take quiz
An empty document is created
Nothing is read, it only points to a path
A listener starts on the collection
A billed read of the document runs
Only documents in the root collection
Only collections
Only documents that already exist
A path whether or not a document exists there
8. How do you add data to Firestore?
There are two main calls. setDoc writes to a document ID you choose, while addDoc creates a new document with an auto-generated ID.
import { doc, setDoc, collection, addDoc } from "firebase/firestore"; // You pick the ID await setDoc(doc(db, "users", "alice"), { name: "Alice", age: 29 }); // Firestore picks the ID const ref = await addDoc(collection(db, "users"), { name: "Bob" }); console.log(ref.id);
Auto IDs are 20 random characters, which spreads writes evenly across the database. Multi-document inserts belong in a batched write or transaction.
Take quiz
deleteDoc
getDoc
addDoc
updateDoc
Overwrites it completely
Merges the new fields in
Stores a second version beside it
Throws an already-exists error
9. How do you read a document in Firestore?
Call getDoc with a document reference. It returns a DocumentSnapshot; check exists() and then use data().
const snap = await getDoc(doc(db, "users", "alice")); if (snap.exists()) { console.log(snap.data().name); } else { console.log("No such document"); }
A missing document is not an error. You get a snapshot with exists() false, and it still counts as one billed read. If you need the data to keep updating, use onSnapshot instead of getDoc.
By default the SDK asks the server and falls back to the cache when offline. Pass the cache source if you deliberately want the local copy, for example on a screen that can tolerate slightly old data.
Take quiz
An empty map with exists() true
A thrown 404 exception
A null pointer
A snapshot where exists() is false
getDoc
onSnapshot
getCountFromServer
getDocs
10. How do you update specific fields of a document?
Use updateDoc with only the fields you want to change. Other fields stay untouched. For nested maps, use dot notation so you do not replace the whole map.
await updateDoc(doc(db, "users", "alice"), { age: 30, "address.city": "Mumbai", // only city changes loginCount: increment(1), tags: arrayUnion("vip"), lastSeen: serverTimestamp() });
updateDoc fails with not-found if the document is missing. When you want create-or-update behaviour, use setDoc with { merge: true }.
Several fields in one call are applied atomically, so readers never see half of the change. Transforms such as increment and arrayUnion are computed on the server, which avoids lost updates when many clients write at once.
Take quiz
Use dot notation: 'address.city'
Call setDoc with that one field and no options
Pass {address: {city: 'Pune'}}, which merges into the map
Use slash notation: 'address/city'
Succeeds and writes nothing
Creates it silently
Fails with a not-found error
Creates it only inside a transaction
11. How do you delete a document or a field in Firestore?
deleteDoc removes a document. To remove just one field, call updateDoc with deleteField().
await deleteDoc(doc(db, "users", "alice")); await updateDoc(doc(db, "users", "bob"), { nickname: deleteField() });
Deleting a document does not delete its subcollections. To clear a whole collection tree, use the Admin SDK recursiveDelete or firebase firestore:delete --recursive. Looping deleteDoc from a client is slow and fragile.
Billing note: every document removed counts as one billed delete, so wiping a large collection costs one delete per document plus the reads needed to find them. For data that expires on a schedule, a TTL policy is usually cheaper to operate than a cleanup script.
Take quiz
arrayRemove on that field
updateDoc with deleteField()
deleteDoc with a field path
Setting the field to null, which erases it
A browser loop of deleteDoc calls
Dropping it through the security rules
deleteDoc on the collection path
Admin SDK recursiveDelete or the CLI --recursive flag
12. What are Firestore security rules?
Security rules are declarative rules, evaluated on Google's servers, that decide whether a request from the web, Android, or iOS SDK is allowed. Without matching rules, access is denied.
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; } } }
Rules can inspect request.auth, resource.data (stored data), and request.resource.data (incoming data). Server libraries such as the Admin SDK use IAM and bypass these rules.
Take quiz
Only for write operations
Only in Datastore mode
No, server libraries use IAM and bypass rules
Yes, exactly like client calls
request.resource.data
resource.data
request.auth.token
database.document
13. What are the two Firestore database modes?
A Firestore database runs in either Native mode or Datastore mode. You choose when creating it, and it cannot be switched afterwards.
| Aspect | Native mode | Datastore mode |
| Target use | Mobile, web, and server apps | Server workloads moving from Cloud Datastore |
| Client SDKs | Yes (web, Android, iOS) | No, server libraries only |
| Real-time listeners | Supported | Not supported |
| Offline cache | Supported | Not available |
| API | Firestore API | Datastore API |
Pick Native for almost any new project, unless you are migrating an existing Datastore application.
Take quiz
Neither, you need Realtime Database
Both equally
Datastore mode
Native mode
Changes based on the SDK you use
Cannot be switched later
Can be toggled in settings anytime
Can be changed once a year
14. What is a real-time listener in Firestore?
A real-time listener is created with onSnapshot. It delivers the current result first and then pushes a new snapshot every time the watched document or query result changes.
const unsubscribe = onSnapshot( query(collection(db, "orders"), where("status", "==", "open")), (snap) => { snap.docChanges().forEach((c) => console.log(c.type, c.doc.id)); } ); // later unsubscribe();
Each change is added, modified, or removed. You are billed one read per document in the initial result and one per later change, so detach listeners when a screen closes.
Take quiz
Call the unsubscribe function returned by onSnapshot
Do nothing, listeners expire after a minute
Delete the query path
Call closeConnection() on db
One read for each full result refresh
One read per minute connected
One read per document that is added or changes in the result
Nothing, updates are free
15. What is offline persistence in Firestore?
Offline persistence keeps a local cache of documents your app has read or listened to. While offline, reads are served from that cache and writes are queued and sent when the connection returns.
It is on by default for Android and iOS. On the web it must be enabled, and recent SDKs do that through the cache settings:
import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager } from "firebase/firestore"; const db = initializeFirestore(app, { localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager() }) });
Listeners fire immediately for local writes, which is called latency compensation. The write promise only resolves once the server acknowledges it.
Take quiz
It is discarded
It is queued locally and synced on reconnect
It blocks the UI thread until online
It throws a network error immediately
None, it is always opt-in
Web browsers
Only the Node.js server SDK
Android and iOS
16. What are the key limits of a Firestore document?
Document design is shaped by a handful of hard and soft limits:
| Limit | Value |
| Document size | 1 MiB |
| Single field value | 1 MiB minus 89 bytes |
| Subcollection depth | 100 levels |
| Map nesting depth | 20 levels |
| Index entries per document | 40,000 |
| Sustained writes to one document | About 1 per second (soft) |
The write guideline matters for hot items such as counters or shared status documents. Past roughly one write per second to the same document you will see contention, so spread load with sharding. Always confirm numbers on the current quotas page since Google adjusts them.
Take quiz
About 10,000
There is no guidance
About one
About 500
100 levels
3 levels
20 levels
1,000 levels
17. How do you query documents with where clauses?
Build a query with query() and one or more where() filters, then run it with getDocs. Multiple filters combine with AND.
const q = query( collection(db, "products"), where("category", "==", "books"), where("price", "<", 500), orderBy("price"), limit(20) ); const snap = await getDocs(q);
Supported operators include ==, !=, <, <=, >, >=, in, not-in, array-contains, and array-contains-any. Mixing equality and range filters on different fields usually needs a composite index.
Take quiz
array-any
in
== (matches the whole array only)
array-contains
A security-rule update
A composite index
No index of any kind
A Cloud Function
18. What are Firestore indexes?
Every Firestore query is answered from an index, a sorted list of field values pointing at documents. Because of that, query time grows with the size of the result set, not with the size of the collection.
Firestore builds single-field indexes automatically for every field (ascending, descending, and array-contains). Queries that combine fields in ways those cannot serve need a manually defined composite index.
Indexes are not free. Each one adds storage cost and a little write latency, so you can add index exemptions for fields you never query, such as large text blobs.
You can check which indexes exist, and whether a composite one is still building, in the Firebase console under Firestore, then Indexes. A new composite index on a large collection can take a while to finish building.
Take quiz
The size of the result set, not the whole collection
The total number of documents scanned
The number of collections in the database
The length of the document ID
Created manually after the first query fails
Available only for string fields
Created automatically for each field
Created only when you request them
19. What is the difference between Firestore and Realtime Database?
Both are Firebase NoSQL databases with live sync, but they model data differently. Realtime Database is one large JSON tree. Firestore uses documents and collections with richer queries.
| Aspect | Cloud Firestore | Realtime Database |
| Data model | Documents in collections | Single JSON tree |
| Queries | Compound filters, sorting, indexed | One sort or filter per query, deep reads |
| Scaling | Automatic, multi-region option | Scale by sharding databases manually |
| Billing | Per read, write, delete, plus storage | Mostly bandwidth and storage |
| Latency | Low | Very low, good for presence |
| Offline | Web, Android, iOS | Android and iOS (web limited) |
Many apps use Firestore as the main store and keep Realtime Database for presence or typing indicators.
Take quiz
Both are identical here
Cloud Firestore
Realtime Database
Neither one
Per index entry
Per listener minute
Per document operation
Bandwidth and storage
20. What is the difference between set, add, and update in Firestore?
All three write data, but they differ in how the document ID is chosen and what happens to existing data.
| Call | Document ID | Existing document |
setDoc |
You supply it | Overwritten, unless merge: true |
addDoc |
Auto-generated | Always creates a new document |
updateDoc |
You supply it | Only listed fields change; fails if missing |
A common upsert pattern is setDoc(ref, data, { merge: true }). It creates the document if needed and leaves fields not in the payload alone.
Choose by intent. Use addDoc for append-only data such as log entries, setDoc when the ID carries meaning (a user uid or an order number), and updateDoc when a missing document should be treated as a bug rather than silently created.
Take quiz
setDoc without options
addDoc
setDoc with { merge: true }
updateDoc, which creates it if missing
Leaves them untouched
Sets them to null
Rejects the write
Deletes them
21. How do Firestore transactions work?
A transaction is a function that reads documents, then writes, and commits everything atomically. If another client changes a document you read before you commit, Firestore re-runs your function.
await runTransaction(db, async (tx) => { const ref = doc(db, "stock", "sku42"); const snap = await tx.get(ref); // reads first const qty = snap.data().qty; if (qty < 1) throw new Error("Out of stock"); tx.update(ref, { qty: qty - 1 }); // writes last });
flowchart TD
A["Start transaction"] --> B["Run function: reads then writes"]
B --> C{Read documents changed by someone else?}
C -- No --> D["Commit all writes atomically"]
C -- Yes --> E{Retries left?}
E -- Yes --> B
E -- No --> F["Fail with error"]
Reads must come before writes. Transactions do not work offline.
Take quiz
Only on the server SDK
In a separate batch
After all writes
Before any writes
Each email would be billed as a write
It may run more than once when contention occurs
Transactions block network calls
Writes would be committed twice
22. What is a batched write in Firestore?
A batched write groups several set, update, and delete operations into one atomic commit. Either all succeed or none are applied. Unlike a transaction, a batch has no reads.
const batch = writeBatch(db); batch.set(doc(db, "orders", "o1"), { total: 40 }); batch.update(doc(db, "users", "alice"), { orders: increment(1) }); batch.delete(doc(db, "carts", "alice")); await batch.commit();
Batches also work offline and are queued like single writes. Older docs capped a batch at 500 operations; check the current quota page, as request size is now the main constraint.
Batches fit fan-out updates, such as writing a post and incrementing the author's post count together. With no read step, a batch never retries on contention, which makes it cheaper than a transaction under heavy load.
Take quiz
None of the writes
The first and third
Only the first
All three, and the error is logged
Yes, using batch.read()
Only in Native mode
No, batches are write-only
Yes, with getDoc inside the batch
23. What is the difference between transactions and batched writes?
Both commit multiple writes atomically. The deciding factor is whether the writes depend on a value you must read first.
| Aspect | Transaction | Batched write |
| Reads allowed | Yes, before writes | No |
| Atomic | Yes | Yes |
| Contention | Retries the function | No retries needed |
| Offline | Fails | Queued and synced |
| Typical use | Check stock then decrement | Update several docs together |
If you only need to push known changes to several documents, use a batch. If the new value depends on the current one, use a transaction or a field transform like increment.
Under heavy contention a transaction keeps retrying, while a batch just commits. If a code review shows a transaction wrapped around writes that never depend on a read, a batch would be simpler and faster.
Take quiz
A plain updateDoc
A transaction
A batched write
A collection group query
Neither
Transactions
Only server SDK transactions
Batched writes, which queue locally
24. How does Firestore pricing work?
Firestore bills per operation rather than per hour of capacity. You pay for document reads, writes, and deletes, plus storage and network egress.
A daily free quota covers 50,000 reads, 20,000 writes, 20,000 deletes, and 1 GiB of storage. Rates vary by location, so check the pricing page.
- A query returning no results still costs one read
- Listeners cost one read per initial document, then one per change
- Storage includes index entries, not just document bodies
- Aggregation queries bill by index entries scanned
Estimate cost by counting documents touched, not API calls. A screen that lists 50 items and opens a listener costs 50 reads on load, plus one read per later change. Multiply by daily active users to forecast the bill, and set budget alerts in Google Cloud Billing to catch surprises early.
Take quiz
As many as documents scanned
Zero
One
One per index entry
Index entry storage
The number of subcollections
Collection names
The size of security rules
25. How do you paginate query results with cursors?
Order the query, set a limit, and pass the last document of the previous page to startAfter.
const first = query(collection(db, "posts"), orderBy("createdAt", "desc"), limit(20)); const page1 = await getDocs(first); const last = page1.docs[page1.docs.length - 1]; const next = query(collection(db, "posts"), orderBy("createdAt", "desc"), startAfter(last), limit(20));
Avoid offset: skipped documents are still billed as reads. If many documents share the same sort value, add the document ID as a final orderBy so page boundaries stay stable.
For live pagination, attach a listener to each page query. For paging backwards, combine limitToLast with endBefore. Keep the last snapshot object rather than just a field value, because the snapshot carries every ordering value the cursor needs.
Take quiz
Cursors are free of charge
Offset only sorts descending
Offset breaks transactions
Skipped documents under offset are still billed as reads
A TTL policy
The document ID as a last sort field
A second limit()
A where clause on the collection name
26. Why does Firestore require composite indexes?
Firestore can only answer a query by reading one contiguous range of an index. A single-field index cannot give you results filtered on one field and sorted by another, so a composite index over both fields is needed.
For example, where('city','==','Pune').orderBy('rating','desc') needs an index on (city, rating). Equality-only filters across fields are different, because Firestore can merge single-field indexes for those.
When a query lacks its index, the SDK error includes a link that creates it. For teams, define indexes in firestore.indexes.json and deploy them with the Firebase CLI.
Index merging has limits. It works for equality filters, but a range filter or an orderBy on a different field breaks the merge. Composite indexes also cost storage and write time, so create only the ones your real queries use.
Take quiz
where('city','==','Pune') plus orderBy('rating','desc')
where('city','==','Pune') and where('type','==','cafe')
orderBy('rating') alone
where('city','==','Pune') alone
Pass them in Admin SDK init options
Embed them in security rules
Keep them in firestore.indexes.json and deploy with the Firebase CLI
Email console screenshots
27. How does Firestore handle OR queries?
Firestore supports OR natively through or() (or Filter.or in server SDKs). Internally it splits the query into disjunctions, runs each against an index, and merges the results.
const q = query(collection(db, "tickets"), or(where("priority", "==", "high"), where("assignee", "==", uid)));
A query can have up to 30 disjunctions. Operators such as in and array-contains-any are really shorthand for OR. Before native support, the workaround was running separate queries and merging results in the client.
A document matching several branches still appears once in the result. Also note that adding orderBy on a field drops documents that lack that field, which can make an OR query look like it is missing rows.
Take quiz
10
30
3
100
Use SQL UNION in the console
Mirror data into Realtime Database
Attach a TTL to each branch
Run separate queries and merge results in the client
28. What is the difference between single-field and composite indexes?
| Aspect | Single-field index | Composite index |
| Fields covered | One field | Two or more fields in a set order |
| Creation | Automatic | Manual (console, CLI, or JSON file) |
| Serves | Simple filters, one sort | Combined filters and sorts |
| Control | Can be exempted per field | Defined per query shape |
Order inside a composite index matters. (city, rating) serves equality on city then sort by rating, but not the reverse.
Disable a single-field index when you never query that field. It cuts storage and write cost, and also avoids hotspots on fields such as sequential timestamps.
A quick check: if a query uses only equality filters, merged single-field indexes can usually serve it. The moment you add a sort or range on another field, expect the error message to ask for a composite index.
Take quiz
Composite indexes
Both need manual creation
Single-field indexes
Neither
You never query that field and want lower write and storage cost
Security rules demand it
You use transactions
The field is a map key
29. How do you use array-contains and array-contains-any?
array-contains matches documents whose array field includes one value. array-contains-any matches if the array includes any of up to 30 values.
where("tags", "array-contains", "java") where("tags", "array-contains-any", ["java", "kotlin"])
There are limits. You can use at most one array-contains per disjunction, and you cannot combine it with array-contains-any in the same disjunction. Equality on the whole array (==) is a different, exact-match filter.
Arrays suit tags and roles, because each element gets its own index entry. Keep them small: huge arrays generate many index entries and push a document toward the 40,000-entry limit. For many-to-many relations that keep growing, use a separate document per relation instead.
Take quiz
== ['java','kotlin']
in on the tags field
array-contains with two values
array-contains-any
Yes, but only with orderBy
No, Firestore rejects that combination
Yes, but only inside transactions
Yes, in any amount
30. How do you atomically increment a counter in Firestore?
Use the increment() field transform. The server applies the change itself, so concurrent updates from many clients do not overwrite each other and you skip the read.
await updateDoc(doc(db, "posts", "p1"), { likes: increment(1) });
If the field is missing or not numeric, it is set to the increment value. Transforms also work offline and replay on reconnect.
The limit is still about one write per second to one document. For busier counters, use shards.
Typical uses are like counts, view counters, and stock adjustments that need no condition. When the update depends on a condition, such as only decrement while above zero, increment alone cannot express it, so switch to a transaction. A decrement is simply increment(-1).
Take quiz
The server applies it atomically, so concurrent updates are not lost
It costs no writes
It bypasses security rules
It caches the value on the client
A string field
A null field
The field starting from the increment value
An error
31. How do security rules use request.auth?
request.auth describes the signed-in Firebase user. It is null for unauthenticated requests, and otherwise exposes uid and token (claims such as email_verified or custom roles).
match /notes/{noteId} { allow read: if request.auth != null; allow write: if request.auth.uid == resource.data.ownerId; }
For role checks you can use custom claims on the token, or look up a role document with get(). Each get() or exists() in a rule counts as a billed document read.
Rules are not filters. A query must itself include a condition, such as where('ownerId','==',uid), that guarantees every possible result passes the rule. Otherwise the whole query is rejected, even if the matching documents would have been allowed.
Take quiz
An object with uid 'guest'
null
false
An empty map with uid ''
It counts only in the emulator
It is free because rules run server-side
It is billed as a write
It counts as a billed document read
32. When should you use subcollections instead of nested maps?
Use a subcollection when the data can grow without bound or needs its own queries. Use a nested map when it is small, bounded, and always read together with the parent.
| Need | Subcollection | Nested map |
| Unbounded growth | Yes | No, 1 MiB cap |
| Query or paginate on its own | Yes | No |
| Independent listener or rules | Yes | No |
| Fetched with parent in one read | No | Yes |
Every write to a map entry also rewrites the whole parent document, which concentrates writes on one hot item. A chat room that stores its messages in a map would hit the size and write limits quickly.
Take quiz
A map keyed by message ID in the room document
An array of messages in the room document
A messages subcollection under each room
One document per user holding all messages
A small, fixed address block always read with the profile
Line items across all customers
Comments on a viral post
Audit log entries
33. Why is denormalization common in Firestore?
Firestore has no joins, and a query only returns documents from one collection. To show a feed in a single read, apps copy the fields they need, such as the author's name, into each post.
The price is update fan-out. When the author renames themselves, every copy must change. Teams handle this with a Cloud Function trigger or a batched write that updates the copies.
The rule of thumb is to denormalize data that is read often and changes rarely, and keep fast-changing data in one place.
Typical copies are the author name on posts, the product title on order lines, and the category name on products. Treat a copy as a snapshot. For an order line, keeping the price at purchase time is actually the correct behaviour.
Take quiz
It reduces document size
Security rules cannot read other documents
Firestore requires it for indexing
To render a feed in one query without joins
Transactions stop working
Changes must be propagated to every copy
You lose automatic indexes
Queries get slower with more fields
34. How do Cloud Functions Firestore triggers work?
A Firestore trigger runs a Cloud Function when a document is created, updated, deleted, or written at a matching path. The path can contain wildcards, such as orders/{orderId}.
const { onDocumentCreated } = require("firebase-functions/v2/firestore"); exports.notifyOrder = onDocumentCreated("orders/{orderId}", (event) => { const order = event.data.data(); console.log("New order", event.params.orderId, order.total); });
Update and write triggers expose before and after snapshots. Delivery is at-least-once, so handlers must be idempotent. A trigger that writes back to its own document can start an infinite loop, so guard with a condition.
Use event.data.before and event.data.after in update triggers to react only when one specific field changed, which also prevents the loop above. Heavy work should be queued as a task so the function returns before its timeout.
Take quiz
At-least-once, so handlers must be idempotent
Exactly-once always
At-most-once
Best-effort with no retries ever
Security rules reject it
Storage is billed twice
An infinite trigger loop
The document gets locked
35. How does the Firestore emulator help with testing?
The Firestore emulator runs a local Firestore instance through the Firebase CLI. Tests read and write against it with no production data and no billing.
firebase emulators:start --only firestore // in app or test setup import { connectFirestoreEmulator } from "firebase/firestore"; connectFirestoreEmulator(db, "localhost", 8080);
It also evaluates your security rules, which you can test with @firebase/rules-unit-testing by acting as different users. The Emulator UI lets you inspect data, and you can export and import seed data between runs.
A typical CI job runs firebase emulators:exec, which starts the emulator, runs the test command, and shuts down. Seed data loads with --import and saves with --export-on-exit, so every developer begins from the same state. Point only test builds at the emulator.
Take quiz
enableNetwork(db)
connectFirestoreEmulator
useLocalCache(db)
initializeApp with debug: true
Needing the Firebase CLI
Having to write security rules
Needing indexes
Touching production data and incurring billing
36. How can you reduce Firestore read costs?
Reads are the usual bill driver, so the goal is to read fewer documents, not just faster ones.
- Use
count(),sum(), oraverage()aggregation queries instead of downloading documents to count them. - Add
limit()and cursor pagination so screens load a page at a time. - Detach listeners when a screen closes and avoid listening to whole large collections.
- Enable offline persistence and use cached reads where fresh data is not required.
- Denormalize hot fields so a screen needs one query instead of many.
- For read-heavy public data, serve data bundles from a CDN instead of querying per user.
Check the usage dashboard first. A single forgotten listener often explains a surprise bill.
Another cheap win is replacing a polling screen with one listener on a narrow query, since it bills only for changes. For public data read by many users, such as a product catalog, one CDN-cached bundle can replace thousands of identical query reads.
Take quiz
getDocs and then .length
A collection group scan
A count() aggregation query
onSnapshot and counting changes
Listeners left attached after the user leaves the screen
Defining security rules
Creating composite indexes
Using document references
37. What is a TTL policy in Firestore?
A TTL (time-to-live) policy tells Firestore to automatically delete documents once a timestamp field in them has passed. You define it per collection group and field.
gcloud firestore fields ttls update expireAt \ --collection-group=sessions --enable-ttl
Deletion is not instant. It usually happens within about a day after expiry, so keep checking the timestamp in queries if stale documents must never show. TTL deletes are billed as normal deletes, and subcollections are not removed with the parent.
It suits sessions, temporary tokens, and short-lived logs.
Two practical notes. The expiry can differ per document, so one session may expire in an hour and another in a month. And if the field is missing or is not a timestamp, that document never expires.
Take quiz
After a fixed 30 days
Exactly at the timestamp second
Only when someone queries it
Not instantly, typically within about a day of expiry
Number of epoch seconds
Timestamp
ISO date string
Map with seconds and nanos
38. How do you back up and restore Firestore?
Firestore offers three routes, and the right one depends on how fast and how granular recovery must be.
| Method | What it does | Restores into |
| Managed export | gcloud firestore export gs://bucket writes data to Cloud Storage |
Import into a database |
| Scheduled backups | Daily or weekly snapshots with retention up to 14 weeks | A new database |
| Point-in-time recovery | Keeps versions for 7 days, one-minute granularity | Read, or export then restore |
Exports bill reads, and backups bill storage. Test a restore before you need one, because restoring scheduled backups creates a new database rather than overwriting the live one.
Match the method to the recovery goal. PITR suits an accidental delete noticed within days, scheduled backups suit disaster recovery and retention rules, and exports suit moving data between projects or into BigQuery. Enable PITR and a schedule before launch, since neither can recover data from before it was turned on.
Take quiz
A new database
A CSV file in Cloud Storage
The same database, overwriting it in place
Realtime Database
24 hours
30 days
7 days
365 days
39. Why should you avoid sequential document IDs in Firestore?
Firestore splits data by key range. If IDs always increase, such as order-000123, every new write lands in the same range, creating a hotspot with higher latency and contention.
Auto-generated IDs are random, so writes spread across many ranges. The same problem hits an indexed timestamp or counter field that only grows, which is why such fields sometimes get an index exemption.
For a brand-new collection under heavy load, follow the 500/50/5 rule: start at 500 operations per second and raise it by 50% every 5 minutes.
If you need ordering, keep a createdAt field and query on it, instead of encoding order in the ID.
Watch for the symptom too: rising write latency and errors concentrated on a narrow key range usually mean a hotspot. Fix it with random IDs, or prefix keys with a hash or shard value so writes spread out.
Take quiz
They skip security-rule checks
They are random, so writes spread across key ranges
They are shorter, so indexes shrink
They are cached on the client
Start at full load at once
Start at 10 ops/sec and double hourly
Start at 5,000 ops/sec and hold
Start at 500 ops/sec and raise 50% every 5 minutes
40. How do you build a scalable distributed counter in Firestore?
A single document handles roughly one sustained write per second, so a hot counter is split into shards. Each increment goes to a random shard, and the total is the sum of all shards.
// counters/likes/shards/{0..N-1} const shard = Math.floor(Math.random() * NUM_SHARDS); await updateDoc(doc(db, "counters/likes/shards", String(shard)), { count: increment(1) }); // read total const snap = await getDocs(collection(db, "counters/likes/shards")); const total = snap.docs.reduce((s, d) => s + d.data().count, 0);
With 50 shards you can sustain around 50 writes per second. Reading costs one read per shard, so you can also use a sum() aggregation query. Start with few shards and add more as traffic grows.
Shard count is a tuning knob. Too few shards means contention, too many makes each read more expensive. Size it from your peak write rate, and you can add shards later because new increments simply pick from the larger range.
Take quiz
Wrap each increment in a transaction on one document
Use a larger document
Spread increments across 50 or more shard documents
Increment a single document faster
Sum the count from every shard
Add a TTL to each shard
Read shard 0 only
Read the largest shard
41. What happens when a Firestore transaction conflicts?
It depends on the SDK. Client SDKs (web, mobile) use optimistic concurrency: if a document you read changes before commit, the transaction rolls back and your function runs again, five attempts by default, then fails with an aborted error.
Server SDKs (Admin, Java, Python, Go) use pessimistic locking. Documents you read are locked until commit, so competing writers wait, with a maximum transaction time of 270 seconds.
To cut conflicts, keep transactions short, touch few documents, and avoid hot documents. Heavily contended counters should be sharded.
A typical failure is many users hitting one hot document, say a shared inventory record. Each retry re-reads and re-runs your logic, and after the allowed attempts the caller sees an aborted error. Catch it and retry later, or redesign the hot spot, for example by sharding.
Take quiz
Retries forever until it succeeds
Returns a partial result
Replays only the failed write
Re-runs the whole update function, five attempts by default
Optimistic version checks only
Pessimistic locks on documents they read
A client-side mutex
A vote across replicas per transaction
42. How does Firestore achieve strong consistency?
A read after a committed write returns that write, because data is replicated synchronously across several zones (or regions) and a write is only committed once a majority of replicas accept it.
Index entries are updated in the same atomic commit as the document, so a query never sees an index that is out of date compared with the data.
| Location type | Replication | Trade-off |
| Regional | Across zones in one region | Lower latency and cost |
| Multi-region | Across several regions | Higher availability, higher write latency and cost |
Client caches are the exception. A listener may show local pending writes, flagged by hasPendingWrites, before the server confirms them.
Strong consistency does not mean zero latency. A read must confirm the latest committed state, and cross-region writes pay a network round trip to reach the replica quorum, which is why multi-region writes are a little slower.
Take quiz
The new value, because reads are strongly consistent
Possibly the old value for a few seconds
Either value at random
The old value until indexes rebuild
Eventual consistency for lower latency
Better queries but no transactions
Higher availability for higher write latency and cost
Lower availability for cheaper writes
43. Explain the lifecycle of a Firestore real-time listener?
A listener moves through attach, initial sync, live updates, possible disconnection, and detach.
sequenceDiagram participant App participant SDK as Firestore SDK participant Server App->>SDK: onSnapshot(query) SDK-->>App: Cached snapshot (fromCache = true) SDK->>Server: Open listen stream Server-->>SDK: Initial results SDK-->>App: Snapshot (fromCache = false) Server-->>SDK: Document changes SDK-->>App: Updated snapshot App->>SDK: unsubscribe() SDK->>Server: Close listen target
- Attach: the SDK first emits cached results if it has any, marked
fromCache: true. - Initial sync: the server sends the full result, billed as one read per document.
- Live updates: each change arrives as added, modified, or removed.
- Local writes: your own writes fire instantly with
hasPendingWrites. - Disconnect: the SDK serves cache and resumes with a token. If offline under 30 minutes, you pay only for changes. Beyond that, it re-reads like a new query.
- Detach: calling the unsubscribe function closes the stream.
Take quiz
The listener has been detached
The data came from the local cache and is not yet confirmed by the server
The query was served from a CDN
The data is guaranteed to be invalid
A flat reconnect fee
Free
Only the changed documents
As a new query, re-reading the whole result set
44. How does Firestore offline sync handle write conflicts?
The SDK applies a write to the local cache immediately, fires listeners, then sends it to the server when online. This is latency compensation.
There is no merge logic for conflicts. When two clients change the same field offline, the last write the server processes wins for that field. A set without merge overwrites the whole document.
- If security rules reject a queued write, the SDK rolls back the local change and listeners see the value revert.
- Transactions need a connection and fail offline.
- Use field transforms such as
incrementandarrayUnionfor conflict-safe updates. serverTimestamp()is estimated locally until the server sets the real value.
For quantities such as stock or a like count, never write a computed number from an offline client. Send increment(1) so the server combines every device's change instead of letting one overwrite the rest.
Take quiz
The one with the lower timestamp
A merge of both values
Whichever write the server processes last
Neither, Firestore raises a conflict error
The local change is rolled back and listeners see it revert
It retries forever
It is forced through as admin
The local value stays permanently
45. How would you model a chat app in Firestore?
Keep a small chat document and put the messages in a subcollection.
erDiagram
CHAT ||--o{ MESSAGE : contains
USER }o--o{ CHAT : participates
chats/{chatId} { participants: [uidA, uidB], lastMessage: "See you", updatedAt: ts } chats/{chatId}/messages/{messageId} { senderId: uidA, text: "Hi", createdAt: serverTimestamp }
- Inbox:
where('participants','array-contains',uid).orderBy('updatedAt','desc')with a composite index. - Thread:
orderBy('createdAt','desc').limit(50)with a listener, thenstartAfterfor older pages. - Store
lastMessageon the chat document so the inbox needs no extra reads. - Rules should allow access only to users listed in
participants. - Typing and presence are short-lived, so put them in Realtime Database or throttle writes.
Control cost early. A listener on a thread bills one read per new message, so load only the latest 50 and fetch older pages on demand. Keep read receipts in one small document per chat member instead of rewriting every message.
Take quiz
getDocs on every chat, then filter in the client
A collection group query on all messages
An unfiltered listener on the whole chats collection
where participants array-contains uid, ordered by updatedAt
Fetch the full messages subcollection per chat
Store lastMessage on the chat document
Run an offset query per chat
Query the newest message of each chat on every render
46. How do you implement full-text search with Firestore?
Firestore has no built-in full-text search. You either approximate it or pair Firestore with a search engine.
- Prefix search: use a range query,
where('name','>=',term).where('name','<=',term + '\uf8ff'). It matches the start of a string only. - Token arrays: store lowercase words in an array and use
array-containsfor exact word hits. - External engine: sync to Algolia, Typesense, Elasticsearch, or Meilisearch through a Firestore trigger or an official extension, and query that engine from the app.
- Semantic search: use Firestore vector search when meaning matters more than keywords.
For serious search needs, the external engine is the usual answer, since it adds ranking, typo tolerance, and highlighting.
Keep search records small. Index only searchable fields such as title and tags, and keep the full record in Firestore. The search call returns document IDs, and the app then fetches each document by ID, which keeps the external index cheap.
Take quiz
A range filter between the prefix and the prefix plus '\uf8ff'
A contains() filter
A LIKE operator
A regex() query operator
Clients dual-write on every screen
Firestore pushes events through TTL
Firestore triggers via Cloud Functions or an extension push changes
The search engine reads your security rules
47. What is vector search in Firestore?
Vector search finds documents whose stored embedding is closest to a query embedding. You store the embedding in a vector field, create a vector index, and call findNearest.
const coll = db.collection("docs"); const result = await coll.findNearest("embedding", FieldValue.vector(queryEmbedding), { limit: 5, distanceMeasure: "COSINE" }).get();
- Distance measures: EUCLIDEAN, COSINE, and DOT_PRODUCT.
- Vectors can have up to 2048 dimensions, and a query returns up to 1000 results.
- You can pre-filter with
whereclauses before the nearest-neighbour step.
Typical uses are semantic search and retrieval for AI assistants.
Combine vector search with metadata filters, for example restricting to a tenant ID before ranking by similarity. Create the index with the same dimension as your embedding model, and regenerate every embedding if you switch models, because vectors from different models cannot be compared.
Take quiz
EUCLIDEAN
COSINE
MANHATTAN
HAMMING
Enable full-text search
Create a TTL policy
Convert vectors to strings
Create a vector index for the field
48. What are aggregation queries in Firestore?
Aggregation queries compute count(), sum(), and average() on the server and return only the result, without downloading the documents.
const snap = await getAggregateFromServer( query(collection(db, "orders"), where("status", "==", "paid")), { total: sum("amount"), orders: count(), avg: average("amount") } ); console.log(snap.data().total);
They are billed at one read per up to 1,000 index entries scanned, with a minimum of one read. Compare that with fetching 50,000 documents just to count them. They need a supporting index and do not support real-time listeners.
Use them for dashboards and totals, such as order count and revenue for a status filter, with one call instead of loading every order. sum and average ignore non-numeric values, so clean data first. For a continuously updating total, keep a running counter document maintained by a trigger.
Take quiz
A live stream of the count
All matching documents plus a count
Only the number, not the documents
Document IDs only
One read per up to 1,000 index entries scanned
A flat write fee
One read per matched document
They are free
49. How do you enforce data validation and roles in security rules?
Validate incoming data with request.resource.data, and gate roles with custom claims or a lookup document.
function isAdmin() { return request.auth.token.admin == true; } match /products/{id} { allow read: if true; allow create: if isAdmin() && request.resource.data.keys().hasOnly(['name','price','createdAt']) && request.resource.data.name is string && request.resource.data.price is number && request.resource.data.price >= 0; allow update: if isAdmin() && request.resource.data.createdAt == resource.data.createdAt; }
hasOnly blocks unexpected fields, and comparing against resource.data makes a field immutable. Custom claims travel in the ID token, so they cost no extra read, but a claim change only applies after the token refreshes. A get() role lookup is current but adds a billed read.
Test rules in the emulator with signed-in, signed-out, and wrong-user cases. Rules that are too permissive rarely fail loudly, so a unit test proving that a non-owner cannot write is the only reliable safeguard.
Take quiz
request.auth.token.keys()
resource.data.size() < 10
request.time < timestamp
request.resource.data.keys().hasOnly([...])
They cannot be set from the Admin SDK
No extra document read per rule evaluation
Changes apply instantly to tokens already issued
They have no size limit
50. How do you migrate from Realtime Database to Firestore?
This is a remodeling job, not a file copy. Realtime Database is one JSON tree, so the data must become documents and collections.
flowchart TD A["Redesign data model"] --> B["Dual-write new changes"] B --> C["Backfill history with a script"] C --> D["Verify counts and samples"] D --> E["Switch reads to Firestore"] E --> F["Retire old paths"]
- Design collections and subcollections around your queries, not around the old tree.
- Dual-write through client code or a Realtime Database trigger so no changes are lost.
- Backfill history by exporting JSON and writing it with the Admin SDK
BulkWriter, which throttles and retries. - Rewrite security rules in the Firestore rules language.
- Verify, switch reads, and keep the old data until you are confident.
Both databases can coexist, so keep Realtime Database for presence if it fits.