Data model & API designs

This post is a part of a series of posts about my design process for a photo gallery service for GDG Hanoi. You can find the introduction post here: Introduction.

Overview

The data flow plays an essential role in forming the bonding structure between components of the service.

The diagrams below illustrate the photo entity’s journey in uploading, deleting, and reading operations.

Uploading

Photo gallery data flow - upload operation
Photo gallery data flow - upload operation

Deleting

Photo gallery data flow - delete operation
Photo gallery data flow - delete operation

Reading

Photo gallery data flow - read operation
Photo gallery data flow - read operation

Responsive image data

Before diving into the data model, I will briefly introduce how I utilize responsive images on a browser.

Because the gallery is designed to serve many images while each image comes with different rendered sizes depending on the screen breakpoints, I decide to use responsive images with the resolution switching technique (using HTML <img>’s srcset and sizes attributes).

An example HTML code for this technique is as follows:

<img
  alt="Alt description"
  decoding="async"
  loading="lazy"
  sizes="34vw"
  srcset="
    /galleries/2026/bwai/sample.gdghanoi.320.webp   320w,
    /galleries/2026/bwai/sample.gdghanoi.512.webp   512w,
    /galleries/2026/bwai/sample.gdghanoi.768.webp   768w,
    /galleries/2026/bwai/sample.gdghanoi.1024.webp 1024w,
    /galleries/2026/bwai/sample.gdghanoi.1280.webp 1280w,
    /galleries/2026/bwai/sample.gdghanoi.1600.webp 1600w,
    /galleries/2026/bwai/sample.gdghanoi.2048.webp 2048w,
    /galleries/2026/bwai/sample.gdghanoi.2560.webp 2560w
  "
  src="/galleries/2026/bwai/sample.gdghanoi.webp"
/>

For those unfamiliar with this concept, responsive images with resolution switching are images that are designed to address the problem:

How to tell the browser to only load the images that provide “just enough” size & quality for the user’s device and network condition, not too big or not too small?

Imagine using a mobile device with only a 360px viewport width; there is no reason to load an image that is 2560px wide. Right?

You can learn more about the responsive images at MDN’s guide.

Schema

Up to this point, I can list the factors that influence the photo entity data:

Based on the factors, I will design the schema via the table below:

AttributesData typesDescriptions
srcstringA base source URL
wintPhoto’s natural width in pixels
hintPhoto’s natural height in pixels
tsintUnix timestamp of photo upload
pageintPage number
codestringGDG Hanoi’s event code
origin_keystringBlob storage key of the original photo
srcsetsarrayList of srcset images
srcsets.[].wintWidth of a srcset image
srcsets.[].hintHeight of a srcset image
srcsets.[].urlstringURL of a srcset image

Key naming convention

On the blob storage side, to look up the source URLs and the files themselves easily, I come up with the following convention:

galleries/{year}/{event_code}/{photo_id}{?size_enum}.{ext}

Where:

Here is a typical example of a photo entity:

Original key: galleries/2026/bwai/abcxyz.png
Resized keys:
  - galleries/2026/bwai/abcxyz.gdghanoi.webp
  - galleries/2026/bwai/abcxyz.gdghanoi.320.webp
  - galleries/2026/bwai/abcxyz.gdghanoi.512.webp
  - galleries/2026/bwai/abcxyz.gdghanoi.768.webp
  - galleries/2026/bwai/abcxyz.gdghanoi.1024.webp
  - galleries/2026/bwai/abcxyz.gdghanoi.1280.webp
  - galleries/2026/bwai/abcxyz.gdghanoi.1600.webp
  - galleries/2026/bwai/abcxyz.gdghanoi.2048.webp
  - galleries/2026/bwai/abcxyz.gdghanoi.2560.webp

Maintaining the page attribute

Because the database (Firestore) does not support offset-based queries, I have to maintain the paging behavior manually via write operations on the photo collection.

Inserting operation

On inserting a new document, I have to do the following steps:

  1. Query the highest active page number.
  2. Decide to use the highest page number, or increment it by one as registering the document to the next page.
  3. Insert the photo document with the decided page number.

Consequently, the service have to perform 1 read and 1 write operation for a single insert.

Deleting operation

This operation is a bit tricky. Because it causes a gap in the actual document count per page.

The good thing is, this gap does not cause any serious effect on UX, so I can let it as be on production.

Later on, I can set up a batch job to reorganize the page numbers if too many documents are deleted and the gap becomes too large to ignore.

API

There are 4 major operations for the gallery service; I will design an API specification for each of them:

The API implementation may be varied depending on the communication protocol between frameworks. Thus, in this section, I conveniently describe the specifications by a consistent language, which is Protocol Buffers.

Uploading photos

This API is used on the uploader frontend component, in which the admin can upload photos directly to the cloud storage with presigned URLs that follow the object key naming convention described earlier.

edition = "2023";

message UploadRequest {
  repeated bytes files = 1;
}

message UploadResponse {
  bool ok = 1;
}

Deleting photos

This API is used on the uploader frontend component, in which the admin can delete photos from the service.

It takes the object’s original key to perform the delete operation on the photo entity. The API does:

  1. Deleting the photo document from the database.
  2. Deleting all the resized photos from the blob storage.
  3. Deleting the original photo from the blob storage.
edition = "2023";

message DeleteRequest {
  string key = 1;
}

message DeleteResponse {
  bool ok = 1;
}

Listing photos by a cursor timestamp

This API is used on the gallery frontend component, in which the gallery is rendered as infinite-scroll mode.

It takes a cursor timestamp as the input and returns a list of photos ordered by the timestamp in descending order. If the cursor is omitted, it fallbacks to the pre-defined latest timestamp and retrieves the first chunk of photos.

Besides the photos list, it also returns the next cursor timestamp to be used for the next query.

edition = "2023";

message Image {
  ...
}

message ListRequest {
  int64 cursor = 1;
  string code = 2;
}

message ListResponse {
  repeated Image records = 1;
  int64 next = 2 [default = -1];
}

Listing photos by a page number

This API is used on the gallery frontend component, in which the gallery is rendered as page-based mode.

Because the page is set statically into a photo document in the database, this API only takes the page number as the input and returns a list of photos containing the given page number.

edition = "2023";

message Image {
  ...
}

message ListRequest {
  int64 page = 1;
  string code = 2;
}

message ListResponse {
  repeated Image records = 1;
}