System Architecture¶
Overview¶
The Pricer EVO platform automates the process of getting the correct image onto every Electronic Shelf Label (ESL) in a store. A label must display the right price, promotion, and product information at all times. When any upstream data changes — a price update, a new campaign, a design change, a new ESL being installed — the platform detects the change, evaluates what to render, produces the image, and delivers it to the physical hardware.
The system is organized in two layers. The Platform Layer (DTOflow) provides the shared data infrastructure: a Spanner-backed DTO store, a Change Queue Service, and a Large File System. Every piece of meaningful state in the system is a DTO stored here. The Functional Layer is a set of microservices that implement the ESL business logic — managing links, evaluating designs, rendering images, and transmitting them to hardware. These services do not call each other directly. They share state by writing DTOs to DTOflow and react to changes from other services by subscribing to DTOs via the Change Queue Service.
This architecture means every service has a narrow, well-defined responsibility: it owns a set of DTOs, writes them when it has new information, and subscribes to the DTOs it needs from other services. Adding a new service or changing a flow does not require wiring up new REST APIs between services; it requires deciding which DTOs the new service consumes and which it produces.
Architecture diagram¶
flowchart TD
subgraph FL["Functional Layer"]
direction TB
subgraph Links["Link & ESL management"]
LR[link-registry]
end
subgraph Design["Design & content"]
SDL[studio-design-library]
SSL[studio-scenario-library]
AL[actions-library]
end
subgraph Evaluation["Evaluation & rendering"]
SLE[studio-link-evaluator]
SR[studio-renderer]
EIRS[ecc-image-render-service]
ELP[ecclink-projector]
EM[eslimage-merger]
end
subgraph Items["Item data"]
IR[item-registry]
end
subgraph Transmission["Transmission (on-premise)"]
PS[pricer-server]
end
end
subgraph PL["Platform Layer (DTOflow)"]
direction LR
DS[(dtoflow-spanner\nDTO store)]
CQS[dtoflow-changequeue\nCQS]
LFS[dtoflow-lfs\nLarge File System]
end
FL -->|write DTOs| DS
DS -->|change notifications| CQS
CQS -->|queue subscriptions| FL
SR -->|image files| LFS
EIRS -->|image files| LFS
LFS -->|file_path refs| DS
The Platform Layer¶
DTOflow is the backbone of the system. It stores all DTO state, notifies subscribers when DTOs change, and provides binary asset storage. All three components are internal to the platform and expose gRPC APIs; functional services never interact with each other except through DTOflow. See Platform overview for a full description of each component.
dtoflow-spanner— the DTO server. Stores the latest version of every DTO. Each DTO type gets a generatedReaderService,WriterService, andHistoryServicegRPC API, produced by thedtoxtemplating tool from the.protodefinitions in this repository. Theidfield of every DTO follows a hierarchical path pattern (t/{tenantid}/s/{storeid}/...) that encodes the tenant and store context.dtoflow-changequeue(CQS) — the Change Queue Service. When a DTO is written, CQS enqueues a change notification into every queue that has subscribed to that DTO type. Services long-poll their queue to receive changes. CQS supports priority ordering and batch consumption so that high-urgency changes (e.g., a price going live) can be processed ahead of lower-priority background work.dtoflow-lfs— the Large File System. Rendered image files and font binaries are stored in LFS. The corresponding DTO (e.g.,eslimage,studioeslimage,font) carries afile_pathfield referencing the LFS location. LFS access is governed by the same DTO ownership rules as the rest of DTOflow.
The Functional Layer¶
The functional layer implements the ESL business logic as a set of independently deployable Cloud Run services. Each service owns a narrow slice of the system: it is the sole writer of its DTOs and subscribes only to the DTOs it needs. The full service catalog is in services/index.md. A few key examples to illustrate the pattern:
| Service | Responsibility | Owns DTOs | Subscribes to |
|---|---|---|---|
link-registry |
Source of record for ESL assignments and item↔ESL↔design mappings | storeesl, link, storeeslstatus | — |
studio-link-evaluator |
Evaluates communicationpack CEL expressions against item data to produce a resolved studiolink | studiolink | communicationpack.v1, link.v2, storeitemvalues.v1 |
studio-renderer |
Renders the Studio-side ESL image from the resolved studiolink + design assets | studioeslimage | studiolink.v1, storeitemvalues.v1, design.v1, storeesl.v1, canvasdesign.v1 |
item-registry |
Manages per-store item data: raw values, property schema, and processing parameters | storeitemvalues, itemproperties, itemprocessingparameters | — |
eslimage-merger |
Merges studio and ECC renderer outputs into the final authoritative eslimage | eslimage | studioeslimage.v1, ecceslimage.v1 |
Key design principles¶
Single DTO ownership
Each DTO type has exactly one service that is allowed to write it. This makes it unambiguous where any piece of state comes from and prevents conflicting writes. For example, only eslimage-merger writes eslimage; only studio-renderer writes studioeslimage. This principle was one of the motivations behind PLT-2487, which eliminated the dual-ownership problem that existed when both studio-renderer and ecc-image-render-service competed to write renderedimage.
Data sharing via DTOflow, not direct service-to-service calls
When studio-link-evaluator needs item data to evaluate a communicationpack expression, it does not call item-registry's API. Instead, item-registry writes storeitemvalues DTOs to DTOflow, and studio-link-evaluator subscribes to storeitemvalues.v1 via CQS. This keeps services decoupled: neither service needs to know the other exists at deploy time, and either can be updated independently.
Hierarchical IDs (tenant → store → entity)
Every DTO ID encodes its place in the data hierarchy as a resource path. The full pattern is t/{tenantid}/s/{storeid}/{entity-type}/{id}, though some DTOs are tenant-global (no store segment) or singletons per store (no trailing ID). Examples: t/acme/s/store01/esls/1234567890 for a storeesl, t/acme/s/store01/eccparameters for the singleton eccparameters DTO for that store. This allows DTOflow to route and filter efficiently, and makes the scope of any DTO self-evident from its ID.
Versioned schemas (breaking changes = new major version file)
Each DTO is defined in a file named {dtoname}.v{N}.proto. When a breaking change is needed, a new file with an incremented major version is introduced alongside the old one. Consumers migrate to the new version on their own schedule. Non-breaking additions within a version require only a minor version bump, tracked via a comment in the message definition and enforced by the CI pipeline (scripts/validate.sh using buf). This allows the platform to evolve without forcing simultaneous deployments across all services.
CQS as the inter-service event bus All inter-service change delivery flows through the Change Queue Service. Internally, CQS uses per-DTO Pub/Sub topics as its persistence backbone — it consumes these topics as fast as possible and then presents changes to consumers in SLA priority order, optionally grouped by tenant/store for batch efficiency. From a consumer's perspective, there is one named queue per service; subscriptions, priorities, and delivery guarantees are all managed within the DTOflow platform and are visible in service configuration files.
Navigation¶
- Platform overview — DTOflow components in detail
- DTO Reference — All DTOs, ID patterns, and owners
- Services — Per-service documentation
- Flows — End-to-end data flow walkthroughs
- Decisions — Architecture decision records