Data Design Development
Data Design Development
The Big Ideas Behind Reliable, Scalable, and Maintainable Systems Second
Edition
With Early Release ebooks, you get books in their earliest form—the
author’s raw and unedited content as they write—so you can take advantage
of these technologies long before the official release of these titles.
Martin Kleppmann
Designing Data-Intensive Applications
by Martin Kleppmann Copyright © 2026 Martin Kleppmann. All rights
reserved.
While the publisher and the authors have used good faith efforts to ensure
that the information and instructions contained in this work are accurate, the
publisher and the authors disclaim all responsibility for errors or omissions,
including without limitation responsibility for damages resulting from the
use of or reliance on this work. Use of the information and instructions
contained in this work is at your own risk. If any code samples or other
technology this work contains or describes is subject to open source
licenses or the intellectual property rights of others, it is your responsibility
to ensure that your use thereof complies with such licenses and/or rights.
978-1-098-11900-3
Dedication
To everyone using technology and data to address the world’s biggest
problems.
Computing is pop culture. […] Pop culture holds a disdain for
history. Pop culture is all about identity and feeling like you’re
participating. It has nothing to do with cooperation, the past or the
future—it’s living in the present. I think the same is true of most
people who write code for money. They have no idea where [their
culture came from].
With Early Release ebooks, you get books in their earliest form—the
author’s raw and unedited content as they write—so you can take advantage
of these technologies long before the official release of these titles.
This will be the 1st chapter of the final book. The GitHub repo for this book
is https://github.com/ept/ddia2-feedback.
If you have comments about how we might improve the content and/or
examples in this book, or if you notice missing material within this chapter,
please reach out on GitHub.
Such applications are typically built from standard building blocks that
provide commonly needed functionality. For example, many applications
need to:
Store data so that they, or another application, can find it again later
(databases)
Remember the result of an expensive operation, to speed up reads
(caches)
Allow users to search data by keyword or filter it in various ways
(search indexes)
Handle events and data changes as soon as they occur (stream
processing)
Periodically crunch a large amount of accumulated data (batch
processing)
This book is a guide to help you make decisions about which technologies
to use and how to combine them. As you will see, there is no one approach
that is fundamentally better than others; everything has pros and cons. With
this book, you will learn to ask the right questions to evaluate and compare
data systems, so that you can figure out which approach will best serve the
needs of your particular application.
We will start our journey by looking at some of the ways that data is
typically used in organizations today. Many of the ideas here have their
origin in enterprise software (i.e., the software needs and engineering
practices of large organizations, such as big corporations and governments),
since historically, only large organizations had the large data volumes that
required sophisticated technical solutions. If your data volume is small
enough, you can simply keep it in a spreadsheet! However, more recently it
has also become common for smaller companies and startups to manage
large data volumes and build data-intensive systems.
One of the key challenges with data systems is that different people need to
do very different things with data. If you are working at a company, you and
your team will have one set of priorities, while another team may have
entirely different goals, although you might even be working with the same
dataset! Moreover, those goals might not be explicitly articulated, which
can lead to misunderstandings and disagreement about the right approach.
To help you understand what choices you can make, this chapter compares
several contrasting concepts, and explores their trade-offs:
Moreover, this chapter will provide you with terminology that we will need
for the rest of the book.
TERMINOLOGY: FRONTENDS AND BACKENDS
Although business analysts and data scientists tend to use different tools
and operate in different ways, they have some things in common: both
perform analytics, which means they look at the data that the users and
backend services have generated, but they generally do not modify this data
(except perhaps for fixing mistakes). They might create derived datasets in
which the original data has been processed in some way. This has led to a
split between two types of systems—a distinction that we will use
throughout this book:
As we shall see in the next section, operational and analytical systems are
often kept separate, for good reasons. As these systems have matured, two
new specialized roles have emerged: data engineers and analytics
engineers. Data engineers are the people who know how to integrate the
operational and the analytical systems, and who take responsibility for the
organization’s data infrastructure more widely [3]. Analytics engineers
model and transform data to make it more useful for end users querying
data in an organization [4].
NOTE
[Link to Come] explores in detail what we mean with a transaction. This chapter uses the term
loosely to refer to low-latency reads and writes.
Even though databases started being used for many different kinds of data
—posts on social media, moves in a game, contacts in an address book, and
many others—the basic access pattern remained similar to processing
business transactions. An operational system typically looks up a small
number of records by some key (this is called a point query). Records are
inserted, updated, or deleted based on the user’s input. Because these
applications are interactive, this access pattern became known as online
transaction processing (OLTP).
The reports that result from these types of queries are important for
business intelligence, helping the management decide what to do next. In
order to differentiate this pattern of using databases from transaction
processing, it has been called online analytic processing (OLAP) [5]. The
difference between OLTP and analytics is not always clear-cut, but some
typical characteristics are listed in Table 1-1.
Table 1-1. Comparing characteristics of operational and analytic systems
The meaning of online in OLAP is unclear; it probably refers to the fact that queries are not just for
predefined reports, but that analysts use the OLAP system interactively for explorative queries.
Data Warehousing
At first, the same databases were used for both transaction processing and
analytic queries. SQL turned out to be quite flexible in this regard: it works
well for both types of queries. Nevertheless, in the late 1980s and early
1990s, there was a trend for companies to stop using their OLTP systems for
analytics purposes, and to run the analytics on a separate database system
instead. This separate database was called a data warehouse.
The data warehouse contains a read-only copy of the data in all the various
OLTP systems in the company. Data is extracted from OLTP databases
(using either a periodic data dump or a continuous stream of updates),
transformed into an analysis-friendly schema, cleaned up, and then loaded
into the data warehouse. This process of getting data into the data
warehouse is known as Extract–Transform–Load (ETL) and is illustrated in
Figure 1-1. Sometimes the order of the transform and load steps is swapped
(i.e., the transformation is done in the data warehouse, after loading),
resulting in ELT.
Figure 1-1. Simplified outline of ETL into a data warehouse.
In some cases the data sources of the ETL processes are external SaaS
products such as customer relationship management (CRM), email
marketing, or credit card processing systems. In those cases, you do not
have direct access to the original database, since it is accessible only via the
software vendor’s API. Bringing the data from these external systems into
your own data warehouse can enable analyses that are not possible via the
SaaS API. ETL for SaaS APIs is often implemented by specialist data
connector services such as Fivetran, Singer, or AirByte.
Some database systems offer hybrid transactional/analytic processing
(HTAP), which aims to enable OLTP and analytics in a single system
without requiring ETL from one system into another [7, 8]. However, many
HTAP systems internally consist of an OLTP system coupled with a
separate analytical system, hidden behind a common interface—so the
distinction beween the two remains important for understanding how these
systems work.
A data warehouse often uses a relational data model that is queried through
SQL (see Chapter 3), perhaps using specialized business intelligence
software. This model works well for the types of queries that business
analysts need to make, but it is less well suited to the needs of data
scientists, who might need to perform tasks such as:
ETL processes have been generalized to data pipelines, and in some cases
the data lake has become an intermediate stop on the path from the
operational systems to the data warehouse. The data lake contains data in a
“raw” form produced by the operational systems, without the
transformation into a relational data warehouse schema. This approach has
the advantage that each consumer of the data can transform the raw data
into a form that best suits their needs. It has been dubbed the sushi
principle: “raw data is better” [14].
Besides loading data from a data lake into a separate data warehouse, it is
also possible to run typical data warehousing workloads (SQL queries and
business analytics) directly on the files in the data lake, alongside data
science/machine learning workloads. This architecture is known as a data
lakehouse, and it requires a query execution engine and a metadata (e.g.,
schema management) layer that extend the data lake’s file storage [15].
Apache Hive, Spark SQL, Presto, and Trino are examples of this approach.
Systems of record
Analytical systems are usually derived data systems, because they are
consumers of data created elsewhere. Operational services may contain a
mixture of systems of record and derived data systems. The systems of
record are the primary databases to which data is first written, whereas the
derived data systems are the indexes and caches that speed up common read
operations, especially for queries that the system of record cannot answer
efficiently.
Most databases, storage engines, and query languages are not inherently a
system of record or a derived system. A database is just a tool: how you use
it is up to you. The distinction between system of record and derived data
system depends not on the tool, but on how you use it in your application.
By being clear about which data is derived from which other data, you can
bring clarity to an otherwise confusing system architecture.
When the data in one system is derived from the data in another, you need a
process for updating the derived data when the original in the system of
record changes. Unfortunately, many databases are designed based on the
assumption that your application only ever needs to use that one database,
and they do not make it easy to integrate multiple systems in order to
propagate such updates. In [Link to Come] we will discuss approaches to
data integration, which allow us to compose multiple data systems to
achieve things that one system alone cannot do.
With software, two important decisions to be made are who builds the
software and who deploys it. There is a spectrum of possibilities that
outsource each decision to various degrees, as illustrated in Figure 1-2. At
one extreme is bespoke software that you write and run in-house; at the
other extreme are widely-used cloud services or Software as a Service
(SaaS) products that are implemented and operated by an external vendor,
and which you only access through a web interface or API.
Seperately from this spectrum there is also the question of how you deploy
services, either in the cloud or on-premises—for example, whether you use
an orchestration framework such as Kubernetes. However, choice of
deployment tooling is out of scope of this book, since other factors have a
greater influence on the architecture of data systems.
On the other hand, if you need a system that you don’t already know how to
deploy and operate, then adopting a cloud service is often easier and
quicker than learning to manage the system yourself. If you have to hire and
train staff specifically to maintain and operate the system, that can get very
expensive. You still need an operations team when you’re using the cloud
(see “Operations in the Cloud Era”), but outsourcing the basic system
administration can free up your team to focus on higher-level concerns.
Cloud services are particularly valuable if the load on your systems varies a
lot over time. If you provision your machines to be able to handle peak
load, but those computing resources are idle most of the time, the system
becomes less cost-effective. In this situation, cloud services have the
advantage that they can make it easier to scale your computing resources up
or down in response to changes in demand.
For example, analytics systems often have extremely variable load: running
a large analytical query quickly requires a lot of computing resources in
parallel, but once the query completes, those resources sit idle until the user
makes the next query. Predefined queries (e.g., for daily reports) can be
enqueued and scheduled to smooth out the load, but for interactive queries,
the faster you want them to complete, the more variable the workload
becomes. If your dataset is so large that querying it quickly requires
significant computing resources, using the cloud can save money, since you
can return unused resources to the provider rather than leaving them idle.
For smaller datasets, this difference is less significant.
The biggest downside of a cloud service is that you have no control over it:
If it is lacking a feature you need, all you can do is to politely ask the
vendor whether they will add it; you generally cannot implement it
yourself.
If the service goes down, all you can do is to wait for it to recover.
If you are using the service in a way that triggers a bug or causes
performance problems, it will be difficult for you to diagnose the issue.
With software that you run yourself, you can get performance metrics
and debugging information from the operating system to help you
understand its behavior, and you can look at the server logs, but with a
service hosted by a vendor you usually do not have access to these
internals.
Moreover, if the service shuts down or becomes unacceptably expensive,
or if the vendor decides to change their product in a way you don’t like,
you are at their mercy—continuing to run an old version of the software
is usually not an option, so you will be forced to migrate to an alternative
service [22]. This risk is mitigated if there are alternative services that
expose a compatible API, but for many cloud services there are no
standard APIs, which raises the cost of switching, making vendor lock-in
a problem.
Despite all these risks, it has become more and more popular for
organizations to build new applications on top of cloud services. However,
cloud services will not subsume all in-house data systems: many older
systems predate the cloud, and for any services that have specialist
requirements that existing cloud services cannot meet, in-house systems
remain necessary. For example, very latency-sensitive applications such as
high-frequency trading require full control of the hardware.
In principle, almost any software that you can self-host could also be
provided as a cloud service, and indeed such managed services are now
available for many popular data systems. However, systems that have been
designed from the ground up to be cloud-native have been shown to have
several advantages: better performance on the same hardware, faster
recovery from failures, being able to quickly scale computing resources to
match the load, and supporting larger datasets [23, 24, 25]. Table 1-2 lists
some examples of both types of systems.
Self-hosted
Category Cloud-native systems
systems
Many self-hosted data systems have very simple system requirements: they
run on a conventional operating system such as Linux or Windows, they
store their data as files on the filesystem, and they communicate via
standard network protocols such as TCP/IP. A few systems depend on
special hardware such as GPUs (for machine learning) or RDMA network
interfaces, but on the whole, self-hosted software tends to use very generic
computing resources: CPU, RAM, a filesystem, and an IP network.
In contrast, the key idea of cloud-native services is to use not only the
computing resources managed by your operating system, but also to build
upon lower-level cloud services to create higher-level services. For
example:
Object storage services such as Amazon S3, Azure Blob Storage, and
Cloudflare R2 store large files. They provide more limited APIs than a
typical filesystem (basic file reads and writes), but they have the
advantage that they hide the underlying physical machines: the service
automatically distributes the data across many machines, so that you
don’t have to worry about running out of disk space on any one machine.
Even if some machines or their disks fail entirely, no data is lost.
Many other services are in turn built upon object storage and other cloud
services: for example, Snowflake is a cloud-based analytic database
(data warehouse) that relies on S3 for data storage [25], and some other
services in turn build upon Snowflake.
As an alternative to local disks, cloud services also offer virtual disk storage
that can be detached from one instance and attached to a different one
(Amazon EBS, Azure managed disks, and persistent disks in Google
Cloud). Such a virtual disk is not actually a physical disk, but rather a cloud
service provided by a separate set of machines, which emulates the
behavior of a disk (a block device, where each block is typically 4 KiB in
size). This technology makes it possible to run traditional disk-based
software in the cloud, but it often suffers from poor performance and poor
scalability [23].
Many cloud services present an API that hides the individual machines that
actually implement the service. For example, cloud storage replaces fixed-
size disks with metered billing, where you can store data without planning
your capacity needs in advance, and you are then charged based on the
space actually used. Moreover, many cloud services remain highly
available, even when individual machines have failed (see “Reliability and
Fault Tolerance”).
With the rise of cloud services, there has been a bifurcation of roles:
operations teams at infrastructure companies specialize in the details of
providing a reliable service to a large number of customers, while the
customers of the service spend as little time and effort as possible on
infrastructure [32].
Scalability
Latency
If you have users around the world, you might want to have servers
at various locations worldwide so that each user can be served from a
datacenter that is geographically close to them. That avoids the users
having to wait for network packets to travel halfway around the
world to answer their requests. See “Describing Performance”.
Elasticity
Legal compliance
Some countries have data residency laws that require data about
people in their jurisdiction to be stored and processed geographically
within that country [37]. The scope of these rules varies—for
example, in some cases it applies only to medical or financial data,
while other cases are broader. A service with users in several such
jurisdictions will therefore have to distribute their data across servers
in several locations.
These reasons apply both to services that you write yourself (application
code) and services consisting of off-the-shelf software (such as databases).
Distributed systems also have downsides. Every request and API call that
goes via the network needs to deal with the possibility of failure: the
network may be interrupted, or the service may be overloaded or crashed,
and therefore any request may time out without receiving a response. In this
case, we don’t know whether the service received the request, and simply
retrying it might not be safe. We will discuss these problems in detail in
[Link to Come].
For all these reasons, if you can do something on a single machine, this is
often much simpler and easier compared to setting up a distributed system
[21]. CPUs, memory, and disks have grown larger, faster, and more reliable.
When combined with single-node databases such as DuckDB, SQLite, and
KùzuDB, many workloads can now run on a single node. We will explore
more on this topic in [Link to Come].
Microservices and Serverless
On the other hand, having many services can itself breed complexity: each
service requires infrastructure for deploying new releases, adjusting the
allocated hardware resources to match the load, collecting logs, monitoring
service health, and alerting an on-call engineer in the case of a problem.
Orchestration frameworks such as Kubernetes have become a popular way
of deploying services, since they provide a foundation for this
infrastructure. Testing a service during development can be complicated,
since you also need to run all the other services that it depends on.
Just like cloud storage replaced capacity planning (deciding in advance how
many disks to buy) with a metered billing model, the serverless approach is
bringing metered billing to code execution: you only pay for the time that
your application code is actually running, rather than having to provision
resources in advance.
One particular concern are systems that store data about people and their
behavior. Since 2018 the General Data Protection Regulation (GDPR) has
given residents of many European countries greater control and legal rights
over their personal data, and similar privacy regulation has been adopted in
various other countries and states around the world, including for example
the California Consumer Privacy Act (CCPA). Regulations around AI, such
as the EU AI Act, place further restrictions on how personal data can be
used.
Moreover, even in areas that are not directly subject to regulation, there is
increasing recognition of the effects that computer systems have on people
and society. Social media has changed how individuals consume news,
which influences their political opinions and hence may affect the outcome
of elections. Automated systems increasingly make decisions that have
profound consequences for individuals, such as deciding who should be
given a loan or insurance coverage, who should be invited to a job
interview, or who should be suspected of a crime [53].
In general, we store data because we think that its value is greater than the
costs of storing it. However, it is worth remembering that the costs of
storage are not just the bill you pay for Amazon S3 or another service: the
cost-benefit calculation should also take into account the risks of liability
and reputational damage if the data were to be leaked or compromised by
adversaries, and the risk of legal costs and fines if the storage and
processing of the data is found not to be compliant with the law.
Once all the risks are taken into account, it might be reasonable to decide
that some data is simply not worth storing, and that it should therefore be
deleted. This principle of data minimization (sometimes known by the
German term Datensparsamkeit) runs counter to the “big data” philosophy
of storing lots of data speculatively in case it turns out to be useful in the
future [55]. But it fits with the GDPR, which mandates that personal data
many only be collected for a specified, explicit purpose, that this data may
not later be used for any other purpose, and that the data must not be kept
for longer than necessary for the purposes for which it was collected [56].
Businesses have also taken notice of privacy and safety concerns. Credit
card companies require payment processing businesses to adhere to strict
payment card industry (PCI) standards. Processors undergo frequent
evaluations from independent auditors to verify continued compliance.
Software vendors have also seen increased scrutiny. Many buyers now
require their vendors to comply with Service Organization Control (SOC)
Type 2 standards. As with PCI compliance, vendors undergo third party
audits to verify adherence.
Summary
The theme of this chapter has been to understand trade-offs: that is, to
recognize that for many questions there is not one right answer, but several
different approaches that each have various pros and cons. We explored
some of the most important choices that affect the architecture of data
systems, and introduced terminology that will be needed throughout the rest
of this book.
Finally, we saw that data systems architecture is determined not only by the
needs of the business deploying the system, but also by privacy regulation
that protects the rights of the people whose data is being processed—an
aspect that many engineers are prone to ignoring. How we translate legal
requirements into technical implementations is not yet well understood, but
it’s important to keep this question in mind as we move through the rest of
this book.
FOOTNOTES
REFERENCES
Richard T. Kouzes, Gordon A. Anderson, Stephen T. Elbert, Ian Gorton, and Deborah K. Gracio. The
Changing Paradigm of Data-Intensive Computing. IEEE Computer, volume 42, issue 1, January
2009. doi:10.1109/MC.2009.26
Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan. Local-first
software: you own your data, in spite of the cloud. At 2019 ACM SIGPLAN International Symposium
on New Ideas, New Paradigms, and Reflections on Programming and Software (Onward!), October
2019. doi:10.1145/3359591.3359737
oe Reis and Matt Housley. Fundamentals of Data Engineering. O’Reilly Media, 2022. ISBN:
9781098108304
Rui Pedro Machado and Helder Russa. Analytics Engineering with SQL and dbt. O’Reilly Media, 2023.
ISBN: 9781098142384
Surajit Chaudhuri and Umeshwar Dayal. An Overview of Data Warehousing and OLAP Technology.
ACM SIGMOD Record, volume 26, issue 1, pages 65–74, March 1997. doi:10.1145/248603.248616
Fatma Özcan, Yuanyuan Tian, and Pinar Tözün. Hybrid Transactional/Analytical Processing: A Survey.
At ACM International Conference on Management of Data (SIGMOD), May 2017.
doi:10.1145/3035918.3054784
Adam Prout, Szu-Po Wang, Joseph Victor, Zhou Sun, Yongzhu Li, Jack Chen, Evan Bergeron, Eric
Hanson, Robert Walzer, Rodrigo Gomes, and Nikita Shamgunov. Cloud-Native Transactions and
Analytics in SingleStore. At International Conference on Management of Data (SIGMOD), June
2022. doi:10.1145/3514221.3526055
Michael Stonebraker and Uğur Çetintemel. ‘One Size Fits All’: An Idea Whose Time Has Come and
Gone. At 21st International Conference on Data Engineering (ICDE), April 2005.
doi:10.1109/ICDE.2005.1
Jeffrey Cohen, Brian Dolan, Mark Dunlap, Joseph M Hellerstein, and Caleb Welton. MAD Skills:
New Analysis Practices for Big Data. Proceedings of the VLDB Endowment, volume 2, issue 2, pages
1481–1492, August 2009. doi:10.14778/1687553.1687576
Dan Olteanu. The Relational Data Borg is Learning. Proceedings of the VLDB Endowment, volume
13, issue 12, August 2020. doi:10.14778/3415478.3415572
Matt Bornstein, Martin Casado, and Jennifer Li. Emerging Architectures for Modern Data
Infrastructure: 2020. future.a16z.com, October 2020. Archived at perma.cc/LF8W-KDCC
Michael Armbrust, Ali Ghodsi, Reynold Xin, and Matei Zaharia. Lakehouse: A New Generation of
Open Platforms that Unify Data Warehousing and Advanced Analytics. At 11th Annual Conference
on Innovative Data Systems Research (CIDR), January 2021.
Tejas Manohar. What is Reverse ETL: A Definition & Why It’s Taking Off. hightouch.io, November
2021. Archived at perma.cc/A7TN-GLYJ
Camille Fournier. Why is it so hard to decide to buy? skamille.medium.com, July 2021. Archived at
perma.cc/6VSG-HQ5X
David Heinemeier Hansson. Why we’re leaving the cloud. world.hey.com, October 2022. Archived at
perma.cc/82E6-UJ65
Nima Badizadegan. Use One Big Server. specbranch.com, August 2022. Archived at
perma.cc/M8NB-95UK
Steve Yegge. Dear Google Cloud: Your Deprecation Policy is Killing You. steve-yegge.medium.com,
August 2020. Archived at perma.cc/KQP9-SPGU
Alexandre Verbitski, Anurag Gupta, Debanjan Saha, Murali Brahmadesam, Kamal Gupta, Raman
Mittal, Sailesh Krishnamurthy, Sandor Maurice, Tengiz Kharatishvili, and Xiaofeng Bao. Amazon
Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases. At ACM
International Conference on Management of Data (SIGMOD), pages 1041–1052, May 2017.
doi:10.1145/3035918.3056101
Panagiotis Antonopoulos, Alex Budovski, Cristian Diaconu, Alejandro Hernandez Saenz, Jack Hu,
Hanuma Kodavalla, Donald Kossmann, Sandeep Lingam, Umar Farooq Minhas, Naveen Prakash,
Vijendra Purohit, Hugh Qu, Chaitanya Sreenivas Ravella, Krystyna Reisteter, Sheetal Shrotri, Dixin
Tang, and Vikram Wakade. Socrates: The New SQL Server in the Cloud. At ACM International
Conference on Management of Data (SIGMOD), pages 1743–1756, June 2019.
doi:10.1145/3299869.3314047
Midhul Vuppalapati, Justin Miron, Rachit Agarwal, Dan Truong, Ashish Motivala, and Thierry
Cruanes. Building An Elastic Query Engine on Disaggregated Storage. At 17th USENIX Symposium
on Networked Systems Design and Implementation (NSDI), February 2020.
Ravi Murthy and Gurmeet Goindi. AlloyDB for PostgreSQL under the hood: Intelligent, database-
aware storage. cloud.google.com, May 2022. Archived at archive.org
Jack Vanlightly. The Architecture of Serverless Data Systems. jack-vanlightly.com, November 2023.
Archived at perma.cc/UDV4-TNJ5
Eric Jonas, Johann Schleier-Smith, Vikram Sreekanti, Chia-Che Tsai, Anurag Khandelwal, Qifan Pu,
Vaishaal Shankar, Joao Carreira, Karl Krauth, Neeraja Yadwadkar, Joseph E Gonzalez, Raluca Ada
Popa, Ion Stoica, David A Patterson. Cloud Programming Simplified: A Berkeley View on Serverless
Computing. arxiv.org, February 2019.
Betsy Beyer, Jennifer Petoff, Chris Jones, and Niall Richard Murphy. Site Reliability Engineering:
How Google Runs Production Systems. O’Reilly Media, 2016. ISBN: 9781491929124
Thomas Limoncelli. The Time I Stole $10,000 from Bell Labs. ACM Queue, volume 18, issue 5,
November 2020. doi:10.1145/3434571.3434773
Charity Majors. The Future of Ops Jobs. acloudguru.com, August 2020. Archived at perma.cc/GRU2-
CZG3
Boris Cherkasky. (Over)Pay As You Go for Your Datastore. medium.com, September 2021. Archived
at perma.cc/Q8TV-2AM2
Shlomi Kushchi. Serverless Doesn’t Mean DevOpsLess or NoOps. thenewstack.io, February 2023.
Archived at perma.cc/3NJR-AYYU
Erik Bernhardsson. Storm in the stratosphere: how the cloud will be reshuffled. erikbern.com,
November 2021. Archived at perma.cc/SYB2-99P3
Benn Stancil. The data OS. benn.substack.com, September 2021. Archived at perma.cc/WQ43-FHS6
Maria Korolov. Data residency laws pushing companies toward residency as a service. csoonline.com,
January 2022. Archived at perma.cc/CHE4-XZZ2
Kousik Nath. These are the numbers every computer engineer should know. freecodecamp.org,
September 2019. Archived at perma.cc/RW73-36RL
Joseph M Hellerstein, Jose Faleiro, Joseph E Gonzalez, Johann Schleier-Smith, Vikram Sreekanti,
Alexey Tumanov, and Chenggang Wu. Serverless Computing: One Step Forward, Two Steps Back.
At Conference on Innovative Data Systems Research (CIDR), January 2019.
Frank McSherry, Michael Isard, and Derek G. Murray. Scalability! But at What COST? At 15th
USENIX Workshop on Hot Topics in Operating Systems (HotOS), May 2015.
Cindy Sridharan. Distributed Systems Observability: A Guide to Building Robust Systems. Report,
O’Reilly Media, May 2018. Archived at perma.cc/M6JL-XKCM
Benjamin H. Sigelman, Luiz André Barroso, Mike Burrows, Pat Stephenson, Manoj Plakal, Donald
Beaver, Saul Jaspan, and Chandan Shanbhag. Dapper, a Large-Scale Distributed Systems Tracing
Infrastructure. Google Technical Report dapper-2010-1, April 2010. Archived at perma.cc/K7KU-
2TMH
Rodrigo Laigner, Yongluan Zhou, Marcos Antonio Vaz Salles, Yijian Liu, and Marcos Kalinowski.
Data management in microservices: State of the practice, challenges, and research directions.
Proceedings of the VLDB Endowment, volume 14, issue 13, pages 3348–3361, September 2021.
doi:10.14778/3484224.3484232
Sam Newman. Building Microservices, second edition. O’Reilly Media, 2021. ISBN: 9781492034025
Mohammad Shahrad, Rodrigo Fonseca, Íñigo Goiri, Gohar Chaudhry, Paul Batum, Jason Cooke,
Eduardo Laureano, Colby Tresness, Mark Russinovich, Ricardo Bianchini. Serverless in the Wild:
Characterizing and Optimizing the Serverless Workload at a Large Cloud Provider. At USENIX
Annual Technical Conference (ATC), July 2020.
Luiz André Barroso, Urs Hölzle, and Parthasarathy Ranganathan. The Datacenter as a Computer:
Designing Warehouse-Scale Machines, third edition. Morgan & Claypool Synthesis Lectures on
Computer Architecture, October 2018. doi:10.2200/S00874ED3V01Y201809CAC046
David Fiala, Frank Mueller, Christian Engelmann, Rolf Riesen, Kurt Ferreira, and Ron Brightwell.
Detection and Correction of Silent Data Corruption for Large-Scale High-Performance Computing,”
at International Conference for High Performance Computing, Networking, Storage and Analysis
(SC), November 2012. doi:10.1109/SC.2012.49
Anna Kornfeld Simpson, Adriana Szekeres, Jacob Nelson, and Irene Zhang. Securing RDMA for
High-Performance Datacenter Storage Systems. At 12th USENIX Workshop on Hot Topics in Cloud
Computing (HotCloud), July 2020.
Arjun Singh, Joon Ong, Amit Agarwal, Glen Anderson, Ashby Armistead, Roy Bannon, Seb Boving,
Gaurav Desai, Bob Felderman, Paulie Germano, Anand Kanagala, Jeff Provost, Jason Simmons,
Eiichi Tanda, Jim Wanderer, Urs Hölzle, Stephen Stuart, and Amin Vahdat. Jupiter Rising: A Decade
of Clos Topologies and Centralized Control in Google’s Datacenter Network. At Annual Conference
of the ACM Special Interest Group on Data Communication (SIGCOMM), August 2015.
doi:10.1145/2785956.2787508
Cathy O’Neil: Weapons of Math Destruction: How Big Data Increases Inequality and Threatens
Democracy. Crown Publishing, 2016. ISBN: 9780553418811
Supreeth Shastri, Vinay Banakar, Melissa Wasserman, Arun Kumar, and Vijay Chidambaram.
Understanding and Benchmarking the Impact of GDPR on Database Systems. Proceedings of the
VLDB Endowment, volume 13, issue 7, pages 1064–1077, March 2020.
doi:10.14778/3384345.3384354
Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General
Data Protection Regulation). Official Journal of the European Union L 119/1, May 2016.
Chapter 2. Defining Nonfunctional
Requirements
The Internet was done so well that most people think of it as a natural
resource like the Pacific Ocean, rather than something that was man-
made. When was the last time a technology with a scale like that was
so error-free?
With Early Release ebooks, you get books in their earliest form—the
author’s raw and unedited content as they write—so you can take advantage
of these technologies long before the official release of these titles.
This will be the 2nd chapter of the final book. The GitHub repo for this
book is https://github.com/ept/ddia2-feedback.
If you have comments about how we might improve the content and/or
examples in this book, or if you notice missing material within this chapter,
please reach out on GitHub.
If you are building an application, you will be driven by a list of
requirements. At the top of your list is most likely the functionality that the
application must offer: what screens and what buttons you need, and what
each operation is supposed to do in order to fulfill the purpose of your
software. These are your functional requirements.
Not all nonfunctional requirements fall within the scope of this book, but
several do. In this chapter we will introduce several technical concepts that
will help you articulate the nonfunctional requirements for your own
systems:
Let’s assume that users make 500 million posts per day, or 5,700 posts per
second on average. Occasionally, the rate can spike as high as 150,000
posts/second [4]. Let’s also assume that the average user follows 200 people
and has 200 followers (although there is a very wide range: most people
have only a handful of followers, and a few celebrities such as Barack
Obama have over 100 million followers).
Figure 2-1. Simple relational schema for a social network in which users can follow each other.
Let’s say the main read operation that our social network must support is
the home timeline, which displays recent posts by people you are following
(for simplicity we will ignore ads, suggested posts from people you are not
following, and other extensions). We could write the following SQL query
to get the home timeline for a particular user:
To execute this query, the database will use the follows table to find
everybody who current_user is following, look up recent posts by
those users, and sort them by timestamp to get the most recent 1,000 posts
by any of the followed users.
Posts are supposed to be timely, so let’s assume that after somebody makes
a post, we want their followers to be able to see it within 5 seconds. One
way of doing that would be for the user’s client to repeat the query above
every 5 seconds while the user is online (this is known as polling). If we
assume that 10 million users are online and logged in at the same time, that
would mean running the query 2 million times per second. Even if you
increase the polling interval, this is a lot.
Moreover, the query above is quite expensive: if you are following 200
people, it needs to fetch a list of recent posts by each of those 200 people,
and merge those lists. 2 million timeline queries per second then means that
the database needs to look up the recent posts from some sender 400 million
times per second—a huge number. And that is the average case. Some users
follow tens of thousands of accounts; for them, this query is very expensive
to execute, and difficult to make fast.
Materializing and Updating Timelines
Imagine that for each user we store a data structure containing their home
timeline, i.e., the recent posts by people they are following. Every time a
user makes a post, we look up all of their followers, and insert that post into
the home timeline of each follower—like delivering a message to a
mailbox. Now when a user logs in, we can simply give them this home
timeline that we precomputed. Moreover, to receive a notification about any
new posts on their timeline, the user’s client simply needs to subscribe to
the stream of posts being added to their home timeline.
The downside of this approach is that we now need to do more work every
time a user makes a post, because the home timelines are derived data that
needs to be updated. The process is illustrated in Figure 2-2. When one
initial request results in several downstream requests being carried out, we
use the term fan-out to describe the factor by which the number of requests
increases.
Figure 2-2. Fan-out: delivering new posts to every follower of the user who made the post.
At a rate of 5,700 posts posted per second, if the average post reaches 200
followers (i.e., a fan-out factor of 200), we will need to do just over 1
million home timeline writes per second. This is a lot, but it’s still a
significant saving compared to the 400 million per-sender post lookups per
second that we would otherwise have to do.
If the rate of posts spikes due to some special event, we don’t have to do the
timeline deliveries immediately—we can enqueue them and accept that it
will temporarily take a bit longer for posts to show up in followers’
timelines. Even during such load spikes, timelines remain fast to load, since
we simply serve them from a cache.
Describing Performance
Most discussions of software performance consider two main types of
metric:
Response time
The elapsed time from the moment when a user makes a request until
they receive the requested answer. The unit of measurement is
seconds.
Throughput
The number of requests per second, or the data volume per second,
that the system is processing. For a given a particular allocation of
hardware resources, there is a maximum throughput that can be
handled. The unit of measurement is “somethings per second”.
In the social network case study, “posts per second” and “timeline writes
per second” are throughput metrics, whereas the “time it takes to load the
home timeline” or the “time until a post is delivered to followers” are
response time metrics.
To avoid retries overloading a service, you can increase and randomize the
time between successive retries on the client side (exponential backoff [8,
9]), and temporarily stop sending requests to a service that has returned
errors or timed out recently (using a circuit breaker [10] or token bucket
algorithm [11]). The server can also detect when it is approaching overload
and start proactively rejecting requests (load shedding [12]), and send back
responses asking clients to slow down (backpressure [1, 13]). The choice of
queueing and load-balancing algorithms can also make a difference [14].
In this section we will focus primarily on response times, and we will return
to throughput and scalability in “Scalability”.
The response time is what the client sees; it includes all delays incurred
anywhere in the system.
The service time is the duration for which the service is actively
processing the user request.
Queueing delays can occur at several points in the flow: for example,
after a request is received, it might need to wait until a CPU is available
before it can be processed; a response packet might need to be buffered
before it is sent over the network if other tasks on the same machine are
sending a lot of data via the outbound network interface.
Latency is a catch-all term for time during which a request is not being
actively processed, i.e., during which it is latent. In particular, network
latency or network delay refers to the time that request and response
spend traveling through the network.
Figure 2-4. Response time, service time, network latency, and queueing delay.
The response time can vary significantly from one request to the next, even
if you keep making the same request over and over again. Many factors can
add random delays: for example, a context switch to a background process,
the loss of a network packet and TCP retransmission, a garbage collection
pause, a page fault forcing a read from disk, mechanical vibrations in the
server rack [15], or many other causes. We will discuss this topic in more
detail in [Link to Come].
Queueing delays often account for a large part of the variability in response
times. As a server can only process a small number of things in parallel
(limited, for example, by its number of CPU cores), it only takes a small
number of slow requests to hold up the processing of subsequent requests—
an effect known as head-of-line blocking. Even if those subsequent requests
have fast service times, the client will see a slow overall response time due
to the time waiting for the prior request to complete. The queueing delay is
not part of the service time, and for this reason it is important to measure
response times on the client side.
Because the response time varies from one request to the next, we need to
think of it not as a single number, but as a distribution of values that you
can measure. In Figure 2-5, each gray bar represents a request to a service,
and its height shows how long that request took. Most requests are
reasonably fast, but there are occasional outliers that take much longer.
Variation in network delay is also known as jitter.
Figure 2-5. Illustrating mean and percentiles: response times for a sample of 100 requests to a
service.
Usually it is better to use percentiles. If you take your list of response times
and sort it from fastest to slowest, then the median is the halfway point: for
example, if your median response time is 200 ms, that means half your
requests return in less than 200 ms, and half your requests take longer than
that. This makes the median a good metric if you want to know how long
users typically have to wait. The median is also known as the 50th
percentile, and sometimes abbreviated as p50.
In order to figure out how bad your outliers are, you can look at higher
percentiles: the 95th, 99th, and 99.9th percentiles are common (abbreviated
p95, p99, and p999). They are the response time thresholds at which 95%,
99%, or 99.9% of requests are faster than that particular threshold. For
example, if the 95th percentile response time is 1.5 seconds, that means 95
out of 100 requests take less than 1.5 seconds, and 5 out of 100 requests
take 1.5 seconds or more. This is illustrated in Figure 2-5.
It seems intuitively obvious that a fast service is better for users than a slow
service [17]. However, it is surprisingly difficult to get hold of reliable data
to quantify the effect that latency has on user behavior.
A more recent Akamai study [21] claims that a 100 ms increase in response
time reduced the conversion rate of e-commerce sites by up to 7%;
however, on closer inspection, the same study reveals that very fast page
load times are also correlated with lower conversion rates! This seemingly
paradoxical result is explained by the fact that the pages that load fastest are
often those that have no useful content (e.g., 404 error pages). However,
since the study makes no effort to separate the effects of page content from
the effects of load time, its results are probably not meaningful.
High percentiles are especially important in backend services that are called
multiple times as part of serving a single end-user request. Even if you
make the calls in parallel, the end-user request still needs to wait for the
slowest of the parallel calls to complete. It takes just one slow call to make
the entire end-user request slow, as illustrated in Figure 2-6. Even if only a
small percentage of backend calls are slow, the chance of getting a slow call
increases if an end-user request requires multiple backend calls, and so a
higher proportion of end-user requests end up being slow (an effect known
as tail latency amplification [23]).
Figure 2-6. When several backend calls are needed to serve a request, it takes just a single slow
backend request to slow down the entire end-user request.
Percentiles are often used in service level objectives (SLOs) and service
level agreements (SLAs) as ways of defining the expected performance and
availability of a service [24]. For example, an SLO may set a target for a
service to have a median response time of less than 200 ms and a 99th
percentile under 1 s, and a target that at least 99.9% of valid requests result
in non-error responses. An SLA is a contract that specifies what happens if
the SLO is not met (for example, customers may be entitled to a refund).
That is the basic idea, at least; in practice, defining good availability metrics
for SLOs and SLAs is not straightforward [25, 26].
COMPUTING PERCENTILES
Fault
Failure
The distinction between fault and failure can be confusing because they are
the same thing, just at different levels. For example, if a hard drive stops
working, we say that the hard drive has failed: if the system consists only of
that one hard drive, it has stopped providing the required service. However,
if the system you’re talking about contains many hard drives, then the
failure of a single hard drive is only a fault from the point of view of the
bigger system, and the bigger system might be able to tolerate that fault by
having a copy of the data on another hard drive.
Fault Tolerance
For example, in the social network case study, a fault that might happen is
that during the fan-out process, a machine involved in updating the
materialized timelines crashes or become unavailable. To make this process
fault-tolerant, we would need to ensure that another machine can take over
this task without missing any posts that should have been delivered, and
without duplicating any posts. (This idea is known as exactly-once
semantics, and we will examine it in detail in [Link to Come].)
Approximately 2–5% of magnetic hard drives fail per year [36, 37]; in a
storage cluster with 10,000 disks, we should therefore expect on average
one disk failure per day. Recent data suggests that disks are getting more
reliable, but failure rates remain significant [38].
Approximately 0.5–1% of solid state drives (SSDs) fail per year [39].
Small numbers of bit errors are corrected automatically [40], but
uncorrectable errors occur approximately once per year per drive, even
in drives that are fairly new (i.e., that have experienced little wear); this
error rate is higher than that of magnetic hard drives [41, 42].
Other hardware components such as power supplies, RAID controllers,
and memory modules also fail, although less frequently than hard drives
[43, 44].
Approximately one in 1,000 machines has a CPU core that occasionally
computes the wrong result, likely due to manufacturing defects [45, 46,
47]. In some cases, an erroneous computation leads to a crash, but in
other cases it leads to a program simply returning the wrong result.
Data in RAM can also be corrupted, either due to random events such as
cosmic rays, or due to permanent physical defects. Even when memory
with error-correcting codes (ECC) is used, more than 1% of machines
encounter an uncorrectable error in a given year, which typically leads to
a crash of the machine and the affected memory module needing to be
replaced [48]. Moreover, certain pathological memory access patterns
can flip bits with high probability [49].
An entire datacenter might become unavailable (for example, due to
power outage or network misconfiguration) or even be permanently
destroyed (for example by fire or flood). Although such large-scale
failures are rare, their impact can be catastrophic if a service cannot
tolerate the loss of a datacenter [50].
These events are rare enough that you often don’t need to worry about them
when working on a small system, as long as you can easily replace
hardware that becomes faulty. However, in a large-scale system, hardware
faults happen often enough that they become part of the normal system
operation.
Software faults
Although hardware failures can be weakly correlated, they are still mostly
independent: for example, if one disk fails, it’s likely that other disks in the
same machine will be fine for another while. On the other hand, software
faults are often very highly correlated, because it is common for many
nodes to run the same software and thus have the same bugs [53, 54]. Such
faults are harder to anticipate, and they tend to cause many more system
failures than uncorrelated hardware faults [43]. For example:
A software bug that causes every node to fail at the same time in
particular circumstances. For example, on June 30, 2012, a leap second
caused many Java applications to hang simultaneously due to a bug in
the Linux kernel, bringing down many Internet services [55]. Due to a
firmware bug, all SSDs of certain models suddenly fail after precisely
32,768 hours of operation (less than 4 years), rendering the data on them
unrecoverable [56].
A runaway process that uses up some shared, limited resource, such as
CPU time, memory, disk space, network bandwidth, or threads [57]. For
example, a process that consumes too much memory while processing a
large request may be killed by the operating system.
A service that the system depends on slows down, becomes
unresponsive, or starts returning corrupted responses.
An interaction between different systems results in emergent behavior
that does not occur when each system was tested in isolation [58].
Cascading failures, where a problem in one component causes another
component to become overloaded and slow down, which in turn brings
down another component [59, 60].
The bugs that cause these kinds of software faults often lie dormant for a
long time until they are triggered by an unusual set of circumstances. In
those circumstances, it is revealed that the software is making some kind of
assumption about its environment—and while that assumption is usually
true, it eventually stops being true for some reason [61, 62].
Humans design and build software systems, and the operators who keep the
systems running are also human. Unlike machines, humans don’t just
follow rules; their strength is being creative and adaptive in getting their job
done. However, this characteristic also leads to unpredictability, and
sometimes mistakes that can lead to failures, despite best intentions. For
example, one study of large internet services found that configuration
changes by operators were the leading cause of outages, whereas hardware
faults (servers or network) played a role in only 10–25% of outages [63].
It is tempting to label such problems as “human error” and to wish that they
could be solved by better controlling human behavior through tighter
procedures and compliance with rules. However, blaming people for
mistakes is counterproductive. What we call “human error” is not really the
cause of an incident, but rather a symptom of a problem with the
sociotechnical system in which people are trying their best to do their jobs
[64].
Reliability is not just for nuclear power stations and air traffic control—
more mundane applications are also expected to work reliably. Bugs in
business applications cause lost productivity (and legal risks if figures are
reported incorrectly), and outages of e-commerce sites can have huge costs
in terms of lost revenue and damage to reputation.
Scalability
Even if a system is working reliably today, that doesn’t mean it will
necessarily work reliably in the future. One common reason for degradation
is increased load: perhaps the system has grown from 10,000 concurrent
users to 100,000 concurrent users, or from 1 million to 10 million. Perhaps
it is processing much larger volumes of data than it did before.
If you are building a new product that currently only has a small number of
users, perhaps at a startup, the overriding engineering goal is usually to
keep the system as simple and flexible as possible, so that you can easily
modify and adapt the features of your product as you learn more about
customers’ needs [70]. In such an environment, it is counterproductive to
worry about hypothetical scale that might be needed in the future: in the
best case, investments in scalability are wasted effort and premature
optimization; in the worst case, they lock you into an inflexible design and
make it harder to evolve your application.
“If the system grows in a particular way, what are our options for coping
with the growth?”
“How can we add computing resources to handle the additional load?”
“Based on current growth projections, when will we hit the limits of our
current architecture?”
First, we need to succinctly describe the current load on the system; only
then can we discuss growth questions (what happens if our load doubles?).
Often this will be a measure of throughput: for example, the number of
requests per second to a service, how many gigabytes of new data arrive per
day, or the number of shopping cart checkouts per hour. Sometimes you
care about the peak of some variable quantity, such as the number of
simultaneously online users in “Case Study: Social Network Home
Timelines”.
Often there are other statistical characteristics of the load that also affect the
access patterns and hence the scalability requirements. For example, you
may need to know the ratio of reads to writes in a database, the hit rate on a
cache, or the number of data items per user (for example, the number of
followers in the social network case study). Perhaps the average case is
what matters for you, or perhaps your bottleneck is dominated by a small
number of extreme cases. It all depends on the details of your particular
application.
Once you have described the load on your system, you can investigate what
happens when the load increases. You can look at it in two ways:
When you increase the load in a certain way and keep the system
resources (CPUs, memory, network bandwidth, etc.) unchanged, how is
the performance of your system affected?
When you increase the load in a certain way, how much do you need to
increase the resources if you want to keep performance unchanged?
Usually our goal is to keep the performance of the system within the
requirements of the SLA (see “Use of Response Time Metrics”) while also
minimizing the cost of running the system. The greater the required
computing resources, the higher the cost. It might be that some types of
hardware are more cost-effective than others, and these factors may change
over time as new types of hardware become available.
If you can double the resources in order to handle twice the load, while
keeping performance the same, we say that you have linear scalability, and
this is considered a good thing. Occasionally it is possible to handle twice
the load with less than double the resources, due to economies of scale or a
better distribution of peak load [71, 72]. Much more likely is that the cost
grows faster than linearly, and there may be many reasons for the
inefficiency. For example, if you have a lot of data, then processing a single
write request may involve more work than if you have a small amount of
data, even if the size of the request is the same.
Shared-Memory, Shared-Disk, and Shared-
Nothing Architecture
Some cloud-native database systems use separate services for storage and
transaction execution (see “Separation of storage and compute”), with
multiple compute nodes sharing access to the same storage service. This
model has some similarity to a shared-disk architecture, but it avoids the
scalability problems of older systems: instead of providing a filesystem
(NAS) or block device (SAN) abstraction, the storage service offers a
specialized API that is designed for the specific needs of the database [75].
Principles for Scalability
Maintainability
Software does not wear out or suffer material fatigue, so it does not break in
the same ways as mechanical objects do. But the requirements for an
application frequently change, the environment that the software runs in
changes (such as its dependencies and the underlying platform), and it has
bugs that need fixing.
It is widely recognized that the majority of the cost of software is not in its
initial development, but in its ongoing maintenance—fixing bugs, keeping
its systems operational, investigating failures, adapting it to new platforms,
modifying it for new use cases, repaying technical debt, and adding new
features [77, 78].
Every system we create today will one day become a legacy system if it is
valuable enough to survive for a long time. In order to minimize the pain
for future generations who need to maintain our software, we should design
it with maintenance concerns in mind. Although we cannot always predict
which decisions might create maintenance headaches in the future, in this
book we will pay attention to several principles that are widely applicable:
Operability
Simplicity
Make it easy for new engineers to understand the system, by
implementing it using well-understood, consistent patterns and
structures, and avoiding unnecessary complexity.
Evolvability
Good operability means making routine tasks easy, allowing the operations
team to focus their efforts on high-value activities. Data systems can do
various things to make routine tasks easy, including [81]:
Small software projects can have delightfully simple and expressive code,
but as projects get larger, they often become very complex and difficult to
understand. This complexity slows down everyone who needs to work on
the system, further increasing the cost of maintenance. A software project
mired in complexity is sometimes described as a big ball of mud [83].
When complexity makes maintenance hard, budgets and schedules are often
overrun. In complex software, there is also a greater risk of introducing
bugs when making a change: when the system is harder for developers to
understand and reason about, hidden assumptions, unintended
consequences, and unexpected interactions are more easily overlooked [62].
Conversely, reducing complexity greatly improves the maintainability of
software, and thus simplicity should be a key goal for the systems we build.
Abstractions for application code, which aim to reduce its complexity, can
be created using methodologies such as design patterns [87] and domain-
driven design (DDD) [88]. This book is not about such application-specific
abstractions, but rather about general-purpose abstractions on top of which
you can build your applications, such as database transactions, indexes, and
event logs. If you want to use techniques such as DDD, you can implement
them on top of the foundations described in this book.
The ease with which you can modify a data system, and adapt it to changing
requirements, is closely linked to its simplicity and its abstractions: simple
and easy-to-understand systems are usually easier to modify than complex
ones. Since this is such an important idea, we will use a different word to
refer to agility on a data system level: evolvability [89].
One major factor that makes change difficult in large systems is when some
action is irreversible, and therefore that action needs to be taken very
carefully [90]. For example, say you are migrating from one database to
another: if you cannot switch back to the old system in case of problems
wth the new one, the stakes are much higher than if you can easily go back.
Minimizing irreversibility improves flexibility.
Summary
In this chapter we examined several examples of nonfunctional
requirements: performance, reliability, scalability, and maintainability.
Through these topics we have also encountered principles and terminology
that we will need throughout the rest of the book. We started with a case
study of how one might implement home timelines in a social network,
which illustrated some of the challenges that arise at scale.
To achieve reliability, you can use fault tolerance techniques, which allow a
system to continue providing its service even if some component (e.g., a
disk, a machine, or another service) is faulty. We saw examples of hardware
faults that can occur, and distinguished them from software faults, which
can be harder to deal with because they are often strongly correlated.
Another aspect of achieving reliability is to build resilience against humans
making mistakes, and we saw blameless postmortems as a technique for
learning from incidents.
REFERENCES
Mike Cvet. How We Learned to Stop Worrying and Love Fan-In at Twitter. At QCon San Francisco,
December 2016.
Raffi Krikorian. Timelines at Scale. At QCon San Francisco, November 2012. Archived at
perma.cc/V9G5-KLYK
Raffi Krikorian. New Tweets per second record, and how! blog.twitter.com, August 2013. Archived at
perma.cc/6JZN-XJYN
Samuel Axon. 3% of Twitter’s Servers Dedicated to Justin Bieber. mashable.com, September 2010.
Archived at perma.cc/F35N-CGVX
Nathan Bronson, Abutalib Aghayev, Aleksey Charapko, and Timothy Zhu. Metastable Failures in
Distributed Systems. At Workshop on Hot Topics in Operating Systems (HotOS), May 2021.
doi:10.1145/3458336.3465286
Marc Brooker. Metastability and Distributed Systems. brooker.co.za, May 2021. Archived at
archive.org
Marc Brooker. Exponential Backoff And Jitter. aws.amazon.com, March 2015. Archived at
perma.cc/R6MS-AZKH
Marc Brooker. What is Backoff For? brooker.co.za, August 2022. Archived at archive.org
Michael T. Nygard. Release It!, 2nd Edition. Pragmatic Bookshelf, January 2018. ISBN:
9781680502398
Marc Brooker. Fixing retries with token buckets and circuit breakers. brooker.co.za, February 2022.
Archived at archive.org
David Yanacek. Using load shedding to avoid overload. Amazon Builders’ Library, aws.amazon.com.
Archived at perma.cc/9SAW-68MP
Dmitry Kopytkov and Patrick Lee. Meet Bandaid, the Dropbox service proxy. dropbox.tech, March
2018. Archived at perma.cc/KUU6-YG4S
Haryadi S. Gunawi, Riza O. Suminto, Russell Sears, Casey Golliher, Swaminathan Sundararaman,
Xing Lin, Tim Emami, Weiguang Sheng, Nematollah Bidokhti, Caitie McCaffrey, Gary Grider, Parks
M. Fields, Kevin Harms, Robert B. Ross, Andree Jacobson, Robert Ricci, Kirk Webb, Peter Alvaro,
H. Birali Runesha, Mingzhe Hao, and Huaicheng Li. Fail-Slow at Scale: Evidence of Hardware
Performance Faults in Large Production Systems. At 16th USENIX Conference on File and Storage
Technologies, February 2018.
Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, Gunavardhan Kakulapati, Avinash Lakshman,
Alex Pilchin, Swaminathan Sivasubramanian, Peter Vosshall, and Werner Vogels. Dynamo:
Amazon’s Highly Available Key-Value Store. At 21st ACM Symposium on Operating Systems
Principles (SOSP), October 2007. doi:10.1145/1294261.1294281
Kathryn Whitenton. The Need for Speed, 23 Years Later. nngroup.com, May 2020. Archived at
perma.cc/C4ER-LZYA
Greg Linden. Marissa Mayer at Web 2.0. glinden.blogspot.com, November 2005. Archived at
perma.cc/V7EA-3VXB
Jake Brutlag. Speed Matters for Google Web Search. services.google.com, June 2009. Archived at
perma.cc/BK7R-X7M2
Eric Schurman and Jake Brutlag. Performance Related Changes and their User Impact. Talk at
Velocity 2009.
Akamai Technologies, Inc. The State of Online Retail Performance. akamai.com, April 2017.
Archived at perma.cc/UEK2-HYCS
Xiao Bai, Ioannis Arapakis, B. Barla Cambazoglu, and Ana Freire. Understanding and Leveraging the
Impact of Response Latency on User Behaviour in Web Search. ACM Transactions on Information
Systems, volume 36, issue 2, article 21, April 2018. doi:10.1145/3106372
Jeffrey Dean and Luiz André Barroso. The Tail at Scale. Communications of the ACM, volume 56,
issue 2, pages 74–80, February 2013. doi:10.1145/2408776.2408794
Alex Hidalgo. Implementing Service Level Objectives: A Practical Guide to SLIs, SLOs, and Error
Budgets. O’Reilly Media, September 2020. ISBN: 1492076813
Jeffrey C. Mogul and John Wilkes. Nines are Not Enough: Meaningful Metrics for Clouds. At 17th
Workshop on Hot Topics in Operating Systems (HotOS), May 2019. doi:10.1145/3317550.3321432
Tamás Hauer, Philipp Hoffmann, John Lunney, Dan Ardelean, and Amer Diwan. Meaningful
Availability. At 17th USENIX Symposium on Networked Systems Design and Implementation
(NSDI), February 2020.
Ted Dunning. The t-digest: Efficient estimates of distributions. Software Impacts, volume 7, article
100049, February 2021. doi:10.1016/j.simpa.2020.100049
David Kohn. How percentile approximation works (and why it’s more useful than averages).
timescale.com, September 2021. Archived at perma.cc/3PDP-NR8B
Heinrich Hartmann and Theo Schlossnagle. Circllhist — A Log-Linear Histogram Data Structure for
IT Infrastructure Monitoring. arxiv.org, January 2020.
Charles Masson, Jee E. Rim, and Homin K. Lee. DDSketch: A Fast and Fully-Mergeable Quantile
Sketch with Relative-Error Guarantees. Proceedings of the VLDB Endowment, volume 12, issue 12,
pages 2195–2205, August 2019. doi:10.14778/3352063.3352135
Baron Schwartz. Why Percentiles Don’t Work the Way You Think. solarwinds.com, November 2016.
Archived at perma.cc/469T-6UGB
Walter L. Heimerdinger and Charles B. Weinstock. A Conceptual Framework for System Fault
Tolerance. Technical Report CMU/SEI-92-TR-033, Software Engineering Institute, Carnegie Mellon
University, October 1992. Archived at perma.cc/GD2V-DMJW
Ding Yuan, Yu Luo, Xin Zhuang, Guilherme Renna Rodrigues, Xu Zhao, Yongle Zhang, Pranay U.
Jain, and Michael Stumm. Simple Testing Can Prevent Most Critical Failures: An Analysis of
Production Failures in Distributed Data-Intensive Systems. At 11th USENIX Symposium on
Operating Systems Design and Implementation (OSDI), October 2014.
Casey Rosenthal and Nora Jones. Chaos Engineering. O’Reilly Media, April 2020. ISBN:
9781492043867
Eduardo Pinheiro, Wolf-Dietrich Weber, and Luiz Andre Barroso. Failure Trends in a Large Disk
Drive Population. At 5th USENIX Conference on File and Storage Technologies (FAST), February
2007.
Bianca Schroeder and Garth A. Gibson. Disk failures in the real world: What does an MTTF of
1,000,000 hours mean to you? At 5th USENIX Conference on File and Storage Technologies (FAST),
February 2007.
Andy Klein. Backblaze Drive Stats for Q2 2021. backblaze.com, August 2021. Archived at
perma.cc/2943-UD5E
Iyswarya Narayanan, Di Wang, Myeongjae Jeon, Bikash Sharma, Laura Caulfield, Anand
Sivasubramaniam, Ben Cutler, Jie Liu, Badriddine Khessib, and Kushagra Vaid. SSD Failures in
Datacenters: What? When? and Why? At 9th ACM International on Systems and Storage Conference
(SYSTOR), June 2016. doi:10.1145/2928275.2928278
Alibaba Cloud Storage Team. Storage System Design Analysis: Factors Affecting NVMe SSD
Performance (1). alibabacloud.com, January 2019. Archived at archive.org
Bianca Schroeder, Raghav Lagisetty, and Arif Merchant. Flash Reliability in Production: The
Expected and the Unexpected. At 14th USENIX Conference on File and Storage Technologies
(FAST), February 2016.
Jacob Alter, Ji Xue, Alma Dimnaku, and Evgenia Smirni. SSD failures in the field: symptoms, causes,
and prediction models. At International Conference for High Performance Computing, Networking,
Storage and Analysis (SC), November 2019. doi:10.1145/3295500.3356172
Daniel Ford, François Labelle, Florentina I. Popovici, Murray Stokely, Van-Anh Truong, Luiz
Barroso, Carrie Grimes, and Sean Quinlan. Availability in Globally Distributed Storage Systems. At
9th USENIX Symposium on Operating Systems Design and Implementation (OSDI), October 2010.
Kashi Venkatesh Vishwanath and Nachiappan Nagappan. Characterizing Cloud Computing Hardware
Reliability. At 1st ACM Symposium on Cloud Computing (SoCC), June 2010.
doi:10.1145/1807128.1807161
Peter H. Hochschild, Paul Turner, Jeffrey C. Mogul, Rama Govindaraju, Parthasarathy Ranganathan,
David E. Culler, and Amin Vahdat. Cores that don’t count. At Workshop on Hot Topics in Operating
Systems (HotOS), June 2021. doi:10.1145/3458336.3465297
Harish Dattatraya Dixit, Sneha Pendharkar, Matt Beadon, Chris Mason, Tejasvi Chakravarthy, Bharath
Muthiah, and Sriram Sankar. Silent Data Corruptions at Scale. arXiv:2102.11245, February 2021.
Diogo Behrens, Marco Serafini, Sergei Arnautov, Flavio P. Junqueira, and Christof Fetzer. Scalable
Error Isolation for Distributed Systems. At 12th USENIX Symposium on Networked Systems Design
and Implementation (NSDI), May 2015.
Bianca Schroeder, Eduardo Pinheiro, and Wolf-Dietrich Weber. DRAM Errors in the Wild: A Large-
Scale Field Study. At 11th International Joint Conference on Measurement and Modeling of
Computer Systems (SIGMETRICS), June 2009. doi:10.1145/1555349.1555372
Yoongu Kim, Ross Daly, Jeremie Kim, Chris Fallin, Ji Hye Lee, Donghyuk Lee, Chris Wilkerson,
Konrad Lai, and Onur Mutlu. Flipping Bits in Memory Without Accessing Them: An Experimental
Study of DRAM Disturbance Errors. At 41st Annual International Symposium on Computer
Architecture (ISCA), June 2014. doi:10.5555/2665671.2665726
Adrian Cockcroft. Failure Modes and Continuous Resilience. adrianco.medium.com, November 2019.
Archived at perma.cc/7SYS-BVJP
Shujie Han, Patrick P. C. Lee, Fan Xu, Yi Liu, Cheng He, and Jiongzhou Liu. An In-Depth Study of
Correlated Failures in Production SSD-Based Data Centers. At 19th USENIX Conference on File and
Storage Technologies (FAST), February 2021.
Edmund B. Nightingale, John R. Douceur, and Vince Orgovan. Cycles, Cells and Platters: An
Empirical Analysis of Hardware Failures on a Million Consumer PCs. At 6th European Conference
on Computer Systems (EuroSys), April 2011. doi:10.1145/1966445.1966477
Haryadi S. Gunawi, Mingzhe Hao, Tanakorn Leesatapornwongsa, Tiratat Patana-anake, Thanh Do,
Jeffry Adityatama, Kurnia J. Eliazar, Agung Laksono, Jeffrey F. Lukman, Vincentius Martin, and
Anang D. Satria. What Bugs Live in the Cloud? At 5th ACM Symposium on Cloud Computing
(SoCC), November 2014. doi:10.1145/2670979.2670986
Jay Kreps. Getting Real About Distributed System Reliability. blog.empathybox.com, March 2012.
Archived at perma.cc/9B5Q-AEBW
Nelson Minar. Leap Second Crashes Half the Internet. somebits.com, July 2012. Archived at
perma.cc/2WB8-D6EU
Hewlett Packard Enterprise. Support Alerts – Customer Bulletin a00092491en_us. support.hpe.com,
November 2019. Archived at perma.cc/S5F6-7ZAC
Lilia Tang, Chaitanya Bhandari, Yongle Zhang, Anna Karanika, Shuyang Ji, Indranil Gupta, and
Tianyin Xu. Fail through the Cracks: Cross-System Interaction Failures in Modern Cloud Systems.
At 18th European Conference on Computer Systems (EuroSys), May 2023.
doi:10.1145/3552326.3587448
Mike Ulrich. Addressing Cascading Failures. In Betsy Beyer, Jennifer Petoff, Chris Jones, and Niall
Richard Murphy (ed). Site Reliability Engineering: How Google Runs Production Systems. O’Reilly
Media, 2016. ISBN: 9781491929124
Richard I. Cook. How Complex Systems Fail. Cognitive Technologies Laboratory, April 2000.
Archived at perma.cc/RDS6-2YVA
David D Woods. STELLA: Report from the SNAFUcatchers Workshop on Coping With Complexity.
snafucatchers.github.io, March 2017. Archived at archive.org
David Oppenheimer, Archana Ganapathi, and David A. Patterson. Why Do Internet Services Fail, and
What Can Be Done About It? At 4th USENIX Symposium on Internet Technologies and Systems
(USITS), March 2003.
Sidney Dekker. The Field Guide to Understanding ‘Human Error’, 3rd Edition. CRC Press,
November 2017. ISBN: 9781472439055
John Allspaw. Blameless PostMortems and a Just Culture. etsy.com, May 2012. Archived at
perma.cc/YMJ7-NTAP
Itzy Sabo. Uptime Guarantees — A Pragmatic Perspective. world.hey.com, March 2023. Archived at
perma.cc/F7TU-78JB
Michael Jurewitz. The Human Impact of Bugs. jury.me, March 2013. Archived at perma.cc/5KQ4-
VDYL
Haroon Siddique and Ben Quinn. Court clears 39 post office operators convicted due to ‘corrupt data’.
theguardian.com, April 2021. Archived at archive.org
Nicholas Bohm, James Christie, Peter Bernard Ladkin, Bev Littlewood, Paul Marshall, Stephen
Mason, Martin Newby, Steven J. Murdoch, Harold Thimbleby, and Martyn Thomas. The legal rule
that computers are presumed to be operating correctly – unforeseen and unjust consequences.
Briefing note, benthamsgaze.org, June 2022. Archived at perma.cc/WQ6X-TMW4
Dan McKinley. Choose Boring Technology. mcfunley.com, March 2015. Archived at perma.cc/7QW7-
J4YP
Andy Warfield. Building and operating a pretty big storage system called S3. allthingsdistributed.com,
July 2023. Archived at perma.cc/7LPK-TP7V
Ben Stopford. Shared Nothing vs. Shared Disk Architectures: An Independent View. benstopford.com,
November 2009. Archived at perma.cc/7BXH-EDUR
Michael Stonebraker. The Case for Shared Nothing. IEEE Database Engineering Bulletin, volume 9,
issue 1, pages 4–9, March 1986.
Panagiotis Antonopoulos, Alex Budovski, Cristian Diaconu, Alejandro Hernandez Saenz, Jack Hu,
Hanuma Kodavalla, Donald Kossmann, Sandeep Lingam, Umar Farooq Minhas, Naveen Prakash,
Vijendra Purohit, Hugh Qu, Chaitanya Sreenivas Ravella, Krystyna Reisteter, Sheetal Shrotri, Dixin
Tang, and Vikram Wakade. Socrates: The New SQL Server in the Cloud. At ACM International
Conference on Management of Data (SIGMOD), pages 1743–1756, June 2019.
doi:10.1145/3299869.3314047
Sam Newman. Building Microservices, second edition. O’Reilly Media, 2021. ISBN: 9781492034025
Nathan Ensmenger. When Good Software Goes Bad: The Surprising Durability of an Ephemeral
Technology. At The Maintainers Conference, April 2016. Archived at perma.cc/ZXT4-HGZB
Robert L. Glass. Facts and Fallacies of Software Engineering. Addison-Wesley Professional, October
2002. ISBN: 9780321117427
Marianne Bellotti. Kill It with Fire. No Starch Press, April 2021. ISBN: 9781718501188
Lisanne Bainbridge. Ironies of automation. Automatica, volume 19, issue 6, pages 775–779,
November 1983. doi:10.1016/0005-1098(83)90046-8
James Hamilton. On Designing and Deploying Internet-Scale Services. At 21st Large Installation
System Administration Conference (LISA), November 2007.
Dotan Horovits. Open Source for Better Observability. horovits.medium.com, October 2021. Archived
at perma.cc/R2HD-U2ZT
Brian Foote and Joseph Yoder. Big Ball of Mud. At 4th Conference on Pattern Languages of
Programs (PLoP), September 1997. Archived at perma.cc/4GUP-2PBV
Marc Brooker. What is a simple system? brooker.co.za, May 2022. Archived at archive.org
Frederick P Brooks. No Silver Bullet – Essence and Accident in Software Engineering. In The
Mythical Man-Month, Anniversary edition, Addison-Wesley, 1995. ISBN: 9780201835953
Dan Luu. Against essential and accidental complexity. danluu.com, December 2020. Archived at
perma.cc/H5ES-69KC
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of
Reusable Object-Oriented Software. Addison-Wesley Professional, October 1994. ISBN:
9780201633610
Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley
Professional, August 2003. ISBN: 9780321125217
Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson. Analyzing Software Evolvability. at 32nd
Annual IEEE International Computer Software and Applications Conference (COMPSAC), July
2008. doi:10.1109/COMPSAC.2008.50
With Early Release ebooks, you get books in their earliest form—the
author’s raw and unedited content as they write—so you can take advantage
of these technologies long before the official release of these titles.
This will be the 3rd chapter of the final book. The GitHub repo for this
book is https://github.com/ept/ddia2-feedback.
If you have comments about how we might improve the content and/or
examples in this book, or if you notice missing material within this chapter,
please reach out on GitHub.
Data models are perhaps the most important part of developing software,
because they have such a profound effect: not only on how the software is
written, but also on how we think about the problem that we are solving.
Most applications are built by layering one data model on top of another.
For each layer, the key question is: how is it represented in terms of the
next-lower layer? For example:
1. As an application developer, you look at the real world (in which there
are people, organizations, goods, actions, money flows, sensors, etc.) and
model it in terms of objects or data structures, and APIs that manipulate
those data structures. Those structures are often specific to your
application.
2. When you want to store those data structures, you express them in terms
of a general-purpose data model, such as JSON or XML documents,
tables in a relational database, or vertices and edges in a graph. Those
data models are the topic of this chapter.
3. The engineers who built your database software decided on a way of
representing that JSON/relational/graph data in terms of bytes in
memory, on disk, or on a network. The representation may allow the data
to be queried, searched, manipulated, and processed in various ways. We
will discuss these storage engine designs in [Link to Come].
4. On yet lower levels, hardware engineers have figured out how to
represent bytes in terms of electrical currents, pulses of light, magnetic
fields, and more.
Several different data models are widely used in practice, often for different
purposes. Some types of data and some queries are easy to express in one
model, and awkward in another. In this chapter we will explore those trade-
offs by comparing the relational model, the document model, graph-based
data models, event sourcing, and dataframes. We will also briefly look at
query languages that allow you to work with these models. This comparison
will help you decide when to use which model.
TERMINOLOGY: DECLARATIVE QUERY LANGUAGES
The relational model was originally a theoretical proposal, and many people
at the time doubted whether it could be implemented efficiently. However,
by the mid-1980s, relational database management systems (RDBMS) and
SQL had become the tools of choice for most people who needed to store
and query data with some kind of regular structure. Many data management
use cases are still dominated by relational data decades later—for example,
business analytics (see “Stars and Snowflakes: Schemas for Analytics”).
Over the years, there have been many competing approaches to data storage
and querying. In the 1970s and early 1980s, the network model and the
hierarchical model were the main alternatives, but the relational model
came to dominate them. Object databases came and went again in the late
1980s and early 1990s. XML databases appeared in the early 2000s, but
have only seen niche adoption. Each competitor to the relational model
generated a lot of hype in its time, but it never lasted [4]. Instead, SQL has
grown to incorporate other data types besides its relational core—for
example, adding support for XML, JSON, and graph data [5].
In the 2010s, NoSQL was the latest buzzword that tried to overthrow the
dominance of relational databases. NoSQL refers not to a single technology,
but a loose set of ideas around new data models, schema flexibility,
scalability, and a move towards open source licensing models. Some
databases branded themselves as NewSQL, as they aim to provide the
scalability of NoSQL systems along with the data model and transactional
guarantees of traditional relational databases. The NoSQL and NewSQL
ideas have been very influential in the design of data systems, but as the
principles have become widely adopted, use of those terms has faded.
The pros and cons of document and relational data have been debated
extensively; let’s examine some of the key points of that debate.
The Object-Relational Mismatch
NOTE
The term impedance mismatch is borrowed from electronics. Every electric circuit has a certain
impedance (resistance to alternating current) on its inputs and outputs. When you connect one
circuit’s output to another one’s input, the power transfer across the connection is maximized if the
output and input impedances of the two circuits match. An impedance mismatch can lead to signal
reflections and other troubles.
ORMs are complex and can’t completely hide the differences between
the two models, so developers still end up having to think about both the
relational and the object representations of the data.
ORMs are generally only used for OLTP app development (see
“Characterizing Analytical and Operational Systems”); data engineers
making the data available for analytics purposes still need to work with
the underlying relational representation, so the design of the relational
schema still matters when using an ORM.
Many ORMs work only with relational OLTP databases. Organizations
with diverse data systems such as search engines, graph databases, and
NoSQL systems might find ORM support lacking.
Some ORMs generate relational schemas automatically, but these might
be awkward for the users who are accessing the relational data directly,
and they might be inefficient on the underlying database. Customizing
the ORM’s schema and query generation can be complex and negate the
benefit of using the ORM in the first place.
ORMs often come with schema migration tools that update database
schemas as model definitions change. Such tools are handy, but should
be used with caution. Migrations on large or high-traffic tables can lock
the entire table for an extended amount of time, resulting in downtime.
Many operations teams prefer to run schema migrations manually,
incrementally, during off peak hours, or with specialized tools. Safe
schema migrations are discussed further in “Schema flexibility in the
document model”.
ORMs make it easy to accidentally write inefficient queries, such as the
N+1 query problem [7]. For example, say you want to display a list of
user comments on a page, so you perform one query that returns N
comments, each containing the ID of its author. To show the name of the
comment author you need to look up the ID in the users table. In hand-
written SQL you would probably perform this join in the query and
return the author name along with each comment, but with an ORM you
might end up making a separate query on the users table for each of the
N comments to look up its author, resulting in N+1 database queries in
total, which is slower than performing the join in the database. To avoid
this problem, you may need to tell the ORM to fetch the author
information at the same time as fetching the comments.
For data that is well suited to a relational model, some kind of translation
between the persistent relational and the in-memory object
representation is inevitable, and ORMs reduce the amount of boilerplate
code required for this translation. Complicated queries may still need to
be handled outside of the ORM, but the ORM can help with the simple
and repetitive cases.
Some ORMs help with caching the results of database queries, which
can help reduce the load on the database.
ORMs can also help with managing schema migrations and other
administrative activities.
The document data model for one-to-many relationships
Not all data lends itself well to a relational representation; let’s look at an
example to explore a limitation of the relational model. Figure 3-1
illustrates how a résumé (a LinkedIn profile) could be expressed in a
relational schema. The profile as a whole can be identified by a unique
identifier, user_id . Fields like first_name and last_name
appear exactly once per user, so they can be modeled as columns on the
users table.
Most people have had more than one job in their career (positions), and
people may have varying numbers of periods of education and any number
of pieces of contact information. One way of representing such one-to-many
relationships is to put positions, education, and contact information in
separate tables, with a foreign key reference to the users table, as in
Figure 3-1.
Figure 3-1. Representing a LinkedIn profile using a relational schema.
{
"user_id": 251,
"first_name": "Barack",
"last_name": "Obama",
"headline": "Former President of the United
"region_id": "us:91",
"photo_url": "/p/7/000/253/05b/308dd6e.jpg",
"positions": [
{"job_title": "President", "organization": "U
{"job_title": "US Senator (D-IL)", "organizat
],
"education": [
{"school_name": "Harvard University", "start
{"school_name": "Columbia University", "start
],
"contact_info": {
"website": "https://barackobama.com",
"twitter": "https://twitter.com/barackobama"
}
}
Some developers feel that the JSON model reduces the impedance
mismatch between the application code and the storage layer. However, as
we shall see in [Link to Come], there are also problems with JSON as a data
encoding format. The lack of a schema is often cited as an advantage; we
will discuss this in “Schema flexibility in the document model”.
The JSON representation has better locality than the multi-table schema in
Figure 3-1 (see “Data locality for reads and writes”). If you want to fetch a
profile in the relational example, you need to either perform multiple
queries (query each table by user_id ) or perform a messy multi-way
join between the users table and its subordinate tables [8]. In the JSON
representation, all the relevant information is in one place, making the
query both faster and simpler.
The one-to-many relationships from the user profile to the user’s positions,
educational history, and contact information imply a tree structure in the
data, and the JSON representation makes this tree structure explicit (see
Figure 3-2).
This type of relationship is sometimes called one-to-few rather than one-to-many, since a résumé
typically has a small number of positions [9, 10]. In sitations where there may be a genuinely large
number of related items—say, comments on a celebrity’s social media post, of which there could be
many thousands—embedding them all in the same document may be too unwieldy, so the relational
approach in Figure 3-1 is preferable.
If the user interface has a free-text field for entering the region, it makes
sense to store it as a plain-text string. But there are advantages to having
standardized lists of geographic regions, and letting users choose from a
drop-down list or autocompleter:
db.users.aggregate([
{ $match: { _id: 251 } },
{ $lookup: {
from: "regions",
localField: "region_id",
foreignField: "_id",
as: "region"
} }
])
Trade-offs of normalization
Perhaps the organization and school should be entities instead, and the
profile should reference their IDs instead of their names? The same
arguments for referencing the ID of a region also apply here. For example,
say we wanted to include the logo of the school or company in addition to
their name:
Besides the cost of performing all these updates, you also need to consider
the consistency of the database if a process crashes halfway through making
its updates. Databases that offer atomic transactions (see [Link to Come])
make it easier to remain consistent, but not all databases offer atomicity
across multiple documents. It is also possible to ensure consistency through
stream processing, which we discuss in [Link to Come].
Normalization tends to be better for OLTP systems, where both reads and
updates need to be fast; analytics systems often fare better with
denormalized data, since they perform updates in bulk, and the performance
of read-only queries is the dominant concern. Moreover, in systems of small
to moderate scale, a normalized data model is often best, because you don’t
have to worry about keeping multiple copies of the data consistent with
each other, and the cost of performing joins is acceptable. However, in very
large-scale systems, the cost of joins can become problematic.
Denormalization in the social networking case study
This means that whenever the timeline is read, the service still needs to
perform two joins: look up the post ID to fetch the actual post content (as
well as statistics such as the number of likes and replies), and look up the
sender’s profile by ID (to get their username, profile picture, and other
details). This process of looking up the human-readable information by ID
is called hydrating the IDs, and it is essentially a join performed in
application code [11].
The reason for storing only IDs in the precomputed timeline is that the data
they refer to is fast-changing: the number of likes and replies may change
multiple times per second on a popular post, and some users regularly
change their username or profile photo. Since the timeline should show the
latest like count and profile picture when it is viewed, it would not make
sense to denormalize this information into the materialized timeline.
Moreover, the storage cost would be increased significantly by such
denormalization.
This example shows that having to perform joins when reading data is not,
as sometimes claimed, an impediment to creating high-performance,
scalable services. Hydrating post ID and user ID is actually a fairly easy
operation to scale, since it parallelizes well, and the cost doesn’t depend on
the number of accounts you are following or the number of followers you
have.
{
"user_id": 251,
"first_name": "Barack",
"last_name": "Obama",
"positions": [
{"start": 2009, "end": 2017, "job_title": "Pr
{"start": 2005, "end": 2008, "job_title": "US
],
...
}
Figure 3-4. Many-to-many relationships in the document model: the data within each dotted box can
be grouped into one document.
In the document model of Example 3-2, the database needs to index the
org_id field of objects inside the positions array. Many document
databases and relational databases with JSON support are able to create
such indexes on values inside a document.
Data warehouses (see “Data Warehousing”) are usually relational, and there
are a few widely-used conventions for the structure of tables in a data
warehouse: a star schema, snowflake schema, dimensional modeling [12],
and one big table (OBT). These structures are optimized for the needs of
business analysts. ETL processes translate data from operational systems
into this schema.
The example schema in Figure 3-5 shows a data warehouse that might be
found at a grocery retailer. At the center of the schema is a so-called fact
table (in this example, it is called fact_sales ). Each row of the fact
table represents an event that occurred at a particular time (here, each row
represents a customer’s purchase of a product). If we were analyzing
website traffic rather than retail sales, each row might represent a page view
or a click by a user.
Some of the columns in the fact table are attributes, such as the price at
which the product was sold and the cost of buying it from the supplier
(allowing the profit margin to be calculated). Other columns in the fact
table are foreign key references to other tables, called dimension tables. As
each row in the fact table represents an event, the dimensions represent the
who, what, where, when, how, and why of the event.
For example, in Figure 3-5, one of the dimensions is the product that was
sold. Each row in the dim_product table represents one type of product
that is for sale, including its stock-keeping unit (SKU), description, brand
name, category, fat content, package size, etc. Each row in the
fact_sales table uses a foreign key to indicate which product was sold
in that particular transaction. Queries often involve multiple joins to
multiple dimension tables.
Even date and time are often represented using dimension tables, because
this allows additional information about dates (such as public holidays) to
be encoded, allowing queries to differentiate between sales on holidays and
non-holidays.
Figure 3-5 is an example of a star schema. The name comes from the fact
that when the table relationships are visualized, the fact table is in the
middle, surrounded by its dimension tables; the connections to these tables
are like the rays of a star.
In a typical data warehouse, tables are often quite wide: fact tables often
have over 100 columns, sometimes several hundred. Dimension tables can
also be wide, as they include all the metadata that may be relevant for
analysis—for example, the dim_store table may include details of
which services are offered at each store, whether it has an in-store bakery,
the square footage, the date when the store was first opened, when it was
last remodeled, how far it is from the nearest highway, etc.
Some data warehouse schemas take denormalization even further and leave
out the dimension tables entirely, folding the information in the dimensions
into denormalized columns on the fact table instead (essentially,
precomputing the join between the fact table and the dimension tables).
This approach is known as one big table (OBT), and while it requires more
storage space, it sometimes enables faster queries [13].
The main arguments in favor of the document data model are schema
flexibility, better performance due to locality, and that for some applications
it is closer to the object model used by the application. The relational model
counters by providing better support for joins, many-to-one, and many-to-
many relationships. Let’s examine these arguments in more detail.
The document model has limitations: for example, you cannot refer directly
to a nested item within a document, but instead you need to say something
like “the second item in the list of positions for user 251”. If you do need to
reference nested items, a relational approach works better, since you can
refer to any item directly by its ID.
Some applications allow the user to choose the order of items: for example,
imagine a to-do list or issue tracker where the user can drag and drop tasks
to reorder them. The document model supports such applications well,
because the items (or their IDs) can simply be stored in a JSON array to
determine their order. In relational databases there isn’t a standard way of
representing such reorderable lists, and various tricks are used: sorting by
an integer column (requiring renumbering when you insert into the middle),
a linked list of IDs, or fractional indexing [14, 15, 16].
Schema flexibility in the document model
The downside of this approach is that every part of your application that
reads from the database now needs to deal with documents in old formats
that may have been written a long time in the past. On the other hand, in a
schema-on-write database, you would typically perform a migration along
the lines of:
There are many different types of objects, and it is not practicable to put
each type of object in its own table.
The structure of the data is determined by external systems over which
you have no control and which may change at any time.
In situations like these, a schema may hurt more than it helps, and
schemaless documents can be a much more natural data model. But in cases
where all records are expected to have the same structure, schemas are a
useful mechanism for documenting and enforcing that structure. We will
discuss schemas and schema evolution in more detail in [Link to Come].
The locality advantage only applies if you need large parts of the document
at the same time. The database typically needs to load the entire document,
which can be wasteful if you only need to access a small part of a large
document. On updates to a document, the entire document usually needs to
be rewritten. For these reasons, it is generally recommended that you keep
documents fairly small and avoid frequent small updates to a document.
However, the idea of storing related data together for locality is not limited
to the document model. For example, Google’s Spanner database offers the
same locality properties in a relational data model, by allowing the schema
to declare that a table’s rows should be interleaved (nested) within a parent
table [25]. Oracle allows the same, using a feature called multi-table index
cluster tables [26]. The column-family concept in the Bigtable data model
(used in Cassandra, HBase, and ScyllaDB), also known as a wide-column
model, has a similar purpose of managing locality [27].
XML databases are often queried using XQuery and XPath, which are
designed to allow complex queries, including joins across multiple
documents, and also format their results as XML [28]. JSON Pointer [29]
and JSONPath [30] provide an equivalent to XPath for JSON. MongoDB’s
aggregation pipeline, whose $lookup operator for joins we saw in
“Normalization, Denormalization, and Joins”, is an example of a query
language for collections of JSON documents.
Let’s look at another example to get a feel for this language—this time an
aggregation, which is especially needed for analytics. Imagine you are a
marine biologist, and you add an observation record to your database every
time you see animals in the ocean. Now you want to generate a report
saying how many sharks you have sighted per month. In PostgreSQL you
might express that query like this:
This query first filters the observations to only show species in the
Sharks family, then groups the observations by the calendar month in
which they occurred, and finally adds up the number of animals seen in all
observations in that month. The same query can be expressed using
MongoDB’s aggregation pipeline as follows:
db.observations.aggregate([
{ $match: { family: "Sharks" } },
{ $group: {
_id: {
year: { $year: "$observationTimesta
month: { $month: "$observationTimesta
},
totalAnimals: { $sum: "$numAnimals" }
} }
]);
NOTE
Codd’s original description of the relational model [3] actually allowed something similar to JSON
within a relational schema. He called it nonsimple domains. The idea was that a value in a row
doesn’t have to just be a primitive datatype like a number or a string, but it could also be a nested
relation (table)—so you can have an arbitrarily nested tree structure as a value, much like the JSON
or XML support that was added to SQL over 30 years later.
But what if many-to-many relationships are very common in your data? The
relational model can handle simple cases of many-to-many relationships,
but as the connections within your data become more complex, it becomes
more natural to start modeling your data as a graph.
Vertices are people, and edges indicate which people know each
other.
Vertices are web pages, and edges indicate HTML links to other
pages.
Vertices are junctions, and edges represent the roads or railway lines
between them.
There are several different, but related, ways of structuring and querying
data in graphs. In this section we will discuss the property graph model
(implemented by Neo4j, Memgraph, KùzuDB [34], and others [35]) and the
triple-store model (implemented by Datomic, AllegroGraph, Blazegraph,
and others). These models are fairly similar in what they can express, and
some graph databases (such as Amazon Neptune) support both models.
We will also look at four query languages for graphs (Cypher, SPARQL,
Datalog, and GraphQL), as well as SQL support for querying graphs. Other
graph query languages exist, such as Gremlin [36], but these will give us a
representative overview.
To illustrate these different languages and models, this section uses the
graph shown in Figure 3-6 as running example. It could be taken from a
social network or a genealogical database: it shows two people, Lucy from
Idaho and Alain from Saint-Lô, France. They are married and living in
London. Each person and each location is represented as a vertex, and the
relationships between them as edges. This example will help demonstrate
some queries that are easy in graph databases, but difficult in other models.
Figure 3-6. Example of graph-structured data (boxes represent vertices, arrows represent edges).
Property Graphs
In the property graph (also known as labeled property graph) model, each
vertex consists of:
A unique identifier
A label (string) to describe what type of object this vertex represents
A set of outgoing edges
A set of incoming edges
A collection of properties (key-value pairs)
A unique identifier
The vertex at which the edge starts (the tail vertex)
The vertex at which the edge ends (the head vertex)
A label to describe the kind of relationship between the two vertices
A collection of properties (key-value pairs)
You can think of a graph store as consisting of two relational tables, one for
vertices and one for edges, as shown in Example 3-3 (this schema uses the
PostgreSQL jsonb datatype to store the properties of each vertex or
edge). The head and tail vertex are stored for each edge; if you want the set
of incoming or outgoing edges for a vertex, you can query the edges
table by head_vertex or tail_vertex , respectively.
Example 3-3. Representing a property graph using a relational schema
1. Any vertex can have an edge connecting it with any other vertex. There
is no schema that restricts which kinds of things can or cannot be
associated.
2. Given any vertex, you can efficiently find both its incoming and its
outgoing edges, and thus traverse the graph—i.e., follow a path through
a chain of vertices—both forward and backward. (That’s why Example
3-3 has indexes on both the tail_vertex and head_vertex
columns.)
3. By using different labels for different kinds of vertices and relationships,
you can store several different kinds of information in a single graph,
while still maintaining a clean data model.
NOTE
A limitation of graph models is that an edge can only associate two vertices with each other, whereas
a relational join table can represent three-way or even higher-degree relationships by having multiple
foreign key references on a single row. Such relationships can be represented in a graph by creating
an additional vertex corresponding to each row of the join table, and edges to/from that vertex, or by
using a hypergraph.
Those features give graphs a great deal of flexibility for data modeling, as
illustrated in Figure 3-6. The figure shows a few things that would be
difficult to express in a traditional relational schema, such as different kinds
of regional structures in different countries (France has départements and
régions, whereas the US has counties and states), quirks of history such as a
country within a country (ignoring for now the intricacies of sovereign
states and nations), and varying granularity of data (Lucy’s current
residence is specified as a city, whereas her place of birth is specified only
at the level of a state).
You could imagine extending the graph to also include many other facts
about Lucy and Alain, or other people. For instance, you could use it to
indicate any food allergies they have (by introducing a vertex for each
allergen, and an edge between a person and an allergen to indicate an
allergy), and link the allergens with a set of vertices that show which foods
contain which substances. Then you could write a query to find out what is
safe for each person to eat. Graphs are good for evolvability: as you add
features to your application, a graph can easily be extended to
accommodate changes in your application’s data structures.
Cypher is a query language for property graphs, originally created for the
Neo4j graph database, and later developed into an open standard as
openCypher [37]. Besides Neo4j, Cypher is supported by Memgraph,
KùzuDB [34], Amazon Neptune, Apache AGE (with storage in
PostgreSQL), and others. It is named after a character in the movie The
Matrix and is not related to ciphers in cryptography [38].
Example 3-4 shows the Cypher query to insert the lefthand portion of
Figure 3-6 into a graph database. The rest of the graph can be added
similarly. Each vertex is given a symbolic name like usa or idaho .
That name is not stored in the database, but only used internally within the
query to create edges between the vertices, using an arrow notation:
(idaho) -[:WITHIN]-> (usa) creates an edge labeled WITHIN ,
with idaho as the tail node and usa as the head node.
CREATE
(namerica :Location {name:'North America', typ
(usa :Location {name:'United States', typ
(idaho :Location {name:'Idaho', typ
(lucy :Person {name:'Lucy' }),
(idaho) -[:WITHIN ]-> (usa) -[:WITHIN]-> (name
(lucy) -[:BORN_IN]-> (idaho)
When all the vertices and edges of Figure 3-6 are added to the database, we
can start asking interesting questions: for example, find the names of all the
people who emigrated from the United States to Europe. That is, find all the
vertices that have a BORN_IN edge to a location within the US, and also a
LIVING_IN edge to a location within Europe, and return the name
property of each of those vertices.
Example 3-5 shows how to express that query in Cypher. The same arrow
notation is used in a MATCH clause to find patterns in the graph:
(person) -[:BORN_IN]-> () matches any two vertices that are
related by an edge labeled BORN_IN . The tail vertex of that edge is bound
to the variable person , and the head vertex is left unnamed.
Example 3-5. Cypher query to find people who emigrated from the US
to Europe
MATCH
(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (:L
(person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (:L
RETURN person.name
There are several possible ways of executing the query. The description
given here suggests that you start by scanning all the people in the database,
examine each person’s birthplace and residence, and return only those
people who meet the criteria.
But equivalently, you could start with the two Location vertices and
work backward. If there is an index on the name property, you can
efficiently find the two vertices representing the US and Europe. Then you
can proceed to find all locations (states, regions, cities, etc.) in the US and
Europe respectively by following all incoming WITHIN edges. Finally,
you can look for people who can be found through an incoming BORN_IN
or LIVES_IN edge at one of the location vertices.
The answer is yes, but with some difficulty. Every edge that you traverse in
a graph query is effectively a join with the edges table. In a relational
database, you usually know in advance which joins you need in your query.
On the other hand, in a graph query, you may need to traverse a variable
number of edges before you find the vertex you’re looking for—that is, the
number of joins is not fixed in advance.
Example 3-6. The same query as Example 3-5, written in SQL using
recursive common table expressions
WITH RECURSIVE
SELECT vertices.properties->>'name'
FROM vertices
-- join to find those people who were both born i
JOIN born_in_usa ON vertices.vertex_id = born
JOIN lives_in_europe ON vertices.vertex_id = live
First find the vertex whose name property has the value "United
States" , and make it the first element of the set of vertices
in_usa .
Do the same starting with the vertex whose name property has the
value "Europe" , and build up the set of vertices in_europe .
Finally, intersect the set of people born in the USA with the set of
people living in Europe, by joining them.
The fact that a 4-line Cypher query requires 31 lines in SQL shows how
much of a difference the right choice of data model and query language can
make. And this is just the beginning; there are more details to consider, e.g.,
around handling cycles, and choosing between breadth-first or depth-first
traversal [39]. Oracle has a different SQL extension for recursive queries,
which it calls hierarchical [40].
However, the situation may be improving: at the time of writing, there are
plans to add a graph query language called GQL to the SQL standard [41,
42], which will provide a syntax inspired by Cypher, GSQL [43], and
PGQL [44].
NOTE
To be precise, databases that offer a triple-like data model often need to store some additional
metadata on each tuple. For example, AWS Neptune uses quads (4-tuples) by adding a graph ID to
each triple [45]; Datomic uses 5-tuples, extending each triple with a transaction ID and a boolean to
indicate deletion [46]. Since these databases retain the basic subject-predicate-object structure
explained above, this book nevertheless calls them triple-stores.
Example 3-7 shows the same data as in Example 3-4, written as triples in a
format called Turtle, a subset of Notation3 (N3) [47].
Example 3-7. A subset of the data in Figure 3-6, represented as Turtle
triples
@prefix : <urn:example:>.
_:lucy a :Person.
_:lucy :name "Lucy".
_:lucy :bornIn _:idaho.
_:idaho a :Location.
_:idaho :name "Idaho".
_:idaho :type "state".
_:idaho :within _:usa.
_:usa a :Location.
_:usa :name "United States".
_:usa :type "country".
_:usa :within _:namerica.
_:namerica a :Location.
_:namerica :name "North America".
_:namerica :type "continent".
Example 3-8. A more concise way of writing the data in Example 3-7
@prefix : <urn:example:>.
_:lucy a :Person; :name "Lucy"; :b
_:idaho a :Location; :name "Idaho"; :t
_:usa a :Location; :name "United States"; :t
_:namerica a :Location; :name "North America"; :t
THE SEMANTIC WEB
Some of the research and development effort on triple stores was motivated
by the Semantic Web, an early-2000s effort to facilitate internet-wide data
exchange by publishing data not only as human-readable web pages, but
also in a standardized, machine-readable format. Although the Semantic
Web as originally envisioned did not succeed [48, 49], the legacy of the
Semantic Web project lives on in a couple of specific technologies: linked
data standards such as JSON-LD [50], ontologies used in biomedical
science [51], Facebook’s Open Graph protocol [52] (which is used for link
unfurling [53]), knowledge graphs such as Wikidata, and standardized
vocabularies for structured data maintained by schema.org .
Triple-stores are another Semantic Web technology that has found use
outside of its original use case: even if you have no interest in the Semantic
Web, triples can be a good internal data model for applications.
<rdf:RDF xmlns="urn:example:"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-s
<Location rdf:nodeID="idaho">
<name>Idaho</name>
<type>state</type>
<within>
<Location rdf:nodeID="usa">
<name>United States</name>
<type>country</type>
<within>
<Location rdf:nodeID="namerica">
<name>North America</name>
<type>continent</type>
</Location>
</within>
</Location>
</within>
</Location>
<Person rdf:nodeID="lucy">
<name>Lucy</name>
<bornIn rdf:nodeID="idaho"/>
</Person>
</rdf:RDF>
RDF has a few quirks due to the fact that it is designed for internet-wide
data exchange. The subject, predicate, and object of a triple are often URIs.
For example, a predicate might be an URI such as <http://my-
company.com/namespace#within> or <http://my-
company.com/namespace#lives_in> , rather than just WITHIN
or LIVES_IN . The reasoning behind this design is that you should be able
to combine your data with someone else’s data, and if they attach a different
meaning to the word within or lives_in , you won’t get a conflict
because their predicates are actually
<http://other.org/foo#within> and
<http://other.org/foo#lives_in> .
SPARQL is a query language for triple-stores using the RDF data model
[55]. (It is an acronym for SPARQL Protocol and RDF Query Language,
pronounced “sparkle.”) It predates Cypher, and since Cypher’s pattern
matching is borrowed from SPARQL, they look quite similar.
The same query as before—finding people who have moved from the US to
Europe—is similarly concise in SPARQL as it is in Cypher (see Example 3-
10).
PREFIX : <urn:example:>
The structure is very similar. The following two expressions are equivalent
(variables start with a question mark in SPARQL):
Because RDF doesn’t distinguish between properties and edges but just
uses predicates for both, you can use the same syntax for matching
properties. In the following expression, the variable usa is bound to any
vertex that has a name property whose value is the string "United
States" :
Example 3-11 shows how to write the data from the left-hand side of Figure
3-6 in Datalog. The edges of the graph ( within , born_in , and
lives_in ) are represented as two-column join tables. For example,
Lucy has the ID 100 and Idaho has the ID 3, so the relationship “Lucy was
born in Idaho” is represented as born_in(100, 3) .
person(100, "Lucy").
born_in(100, 3). /* Lucy was born in Idaho */
Now that we have defined the data, we can write the same query as before,
as shown in Example 3-12. It looks a bit different from the equivalent in
Cypher or SPARQL, but don’t let that put you off. Datalog is a subset of
Prolog, a programming language that you might have seen before if you’ve
studied computer science.
Cypher and SPARQL jump in right away with SELECT , but Datalog takes
a small step at a time. We define rules that derive new virtual tables from
the underlying facts. These derived tables are like (virtual) SQL views: they
are not stored in the database, but you can query them in the same way as a
table containing stored facts.
The content of a virtual table is defined by the part of the rule after the :-
symbol, where we try to find rows that match a certain pattern in the tables.
For example, person(PersonID, PName) matches the row
person(100, "Lucy") , with the variable PersonID bound to the
value 100 and the variable PName bound to the value "Lucy" . A rule
applies if the system can find a match for all patterns on the righthand side
of the :- operator. When the rule applies, it’s as though the lefthand side
of the :- was added to the database (with variables replaced by the values
they matched).
One possible way of applying the rules is thus (and as illustrated in Figure
3-7):
Now rule 3 can find people who were born in some location BornIn and
live in some location LivingIn . Rule 4 invokes rule 3 with BornIn =
'United States' and LivingIn = 'Europe' , and returns only
the names of the people who match the search. By querying the contents of
the virtual us_to_europe table, the Datalog system finally gets the
same answer as in the earlier Cypher and SPARQL queries.
query ChatApp {
channels {
name
recentMessages(latest: 50) {
timestamp
content
sender {
fullName
imageUrl
}
replyTo {
content
sender {
fullName
}
}
}
}
}
Example 3-14 shows what a response to the query in Example 3-13 might
look like. The response is a JSON document that mirrors the structure of the
query: it contains exactly those attributes that were requested, no more and
no less. This approach has the advantage that the server does not need to
know which attributes the client requires in order to render the user
interface; instead, the client can simply request what it needs. For example,
this query does not request a profile picture URL for the sender of the
replyTo message, but if the user interface were changed to add that
profile picture, it would be easy for the client to add the required
imageUrl attribute to the query without changing the server.
{
"data": {
"channels": [
{
"name": "#general",
"recentMessages": [
{
"timestamp": 1693143014,
"content": "Hey! How are y'all doing?
"sender": {"fullName": "Aaliyah", "im
"replyTo": null
},
{
"timestamp": 1693143024,
"content": "Great! And you?",
"sender": {"fullName": "Caleb", "imag
"replyTo": {
"content": "Hey! How are y'all doin
"sender": {"fullName": "Aaliyah"}
}
},
...
The server’s database can store the data in a more normalized form, and
perform the necessary joins to process a query. For example, the server
might store a message along with the user ID of the sender and the ID of the
message it is replying to; when it receives a query like the one above, the
server would then resolve those IDs to find the records they refer to.
However, the client can only ask the server to perform joins that are
explicitly offered in the GraphQL schema.
We previously saw this idea in “Systems of Record and Derived Data”, and
ETL (see “Data Warehousing”) is one example of such a derivation process.
Now we will take the idea further. If we are going to derive one data
representation from another anyway, we can choose different
representations that are optimized for writing and for reading, respectively.
How would you model your data if you only wanted to optimize it for
writing, and if efficient queries were of no concern?
Perhaps the simplest, fastest, and most expressive way of writing data is an
event log: every time you want to write some data, you encode it as a self-
contained string (perhaps as JSON), including a timestamp, and then
append it to a sequence of events. Events in this log are immutable: you
never change or delete them, you only ever append more events to the log
(which may supersede earlier events). An event can contain arbitrary
properties.
Figure 3-8. Using a log of immutable events as source of truth, and deriving materialized views from
it.
In Figure 3-8, every change to the state of the conference (such as the
organizer opening registrations, or attendees making and cancelling
registrations) is first stored as an event. Whenever an event is appended to
the log, several materialized views (also known as projections or read
models) are also updated to reflect the effect of that event. In the conference
example, there might be one materialized view that collects all information
related to the status of each booking, another that computes charts for the
conference organizer’s dashboard, and a third that generates files for the
printer that produces the attendees’ badges.
The idea of using events as the source of truth, and expressing every state
change as an event, is known as event sourcing [61, 62]. The principle of
maintaining separate read-optimized representations and deriving them
from the write-optimized representation is called command query
responsibility segregation (CQRS) [63]. These terms originated in the
domain-driven design (DDD) community, although similar ideas have been
around for a long time, for example in state machine replication (see [Link
to Come]).
When a request from a user comes in, it is called a command, and it first
needs to be validated. Only once the command has been executed and it has
been determined to be valid (e.g., there were enough available seats for a
requested reservation), it becomes a fact, and the corresponding event is
added to the log. Consequently, the event log should contain only valid
events, and a consumer of the event log that builds a materialized view is
not allowed to reject an event.
When modelling your data in an event sourcing style, it is recommended
that you name your events in the past tense (e.g., “the seats were booked”),
because an event is a record of the fact that something has happened in the
past. Even if the user later decides to change or cancel, the fact remains true
that they formerly held a booking, and the change or cancellation is a
separate event that is added later.
For the people developing the system, events better communicate the
intent of why something happened. For example, it’s easier to understand
the event “the booking was cancelled” than “the active column on
row 4001 of the bookings table was set to false , three rows
associated with that booking were deleted from the
seat_assignments table, and a row representing the refund was
inserted into the payments table”. Those row modifications may still
happen when a materialized view processes the cancellation event, but
when they are driven by an event, the reason for the updates becomes
much clearer.
A key principle of event sourcing is that the materialized views are
derived from the event log in a reproducible way: you should always be
able to delete the materialized views and recompute them by processing
the same events in the same order, using the same code. If there was a
bug in the view maintenance code, you can just delete the view and
recompute it with the new code. It’s also easier to find the bug because
you can re-run the view maintenance code as often as you like and
inspect its behavior.
You can have multiple materialized views that are optimized for the
particular queries that your application requires. They can be stored
either in the same database as the events or a different one, depending on
your needs. They can use any data model, and they can be denormalized
for fast reads. You can even keep a view only in memory and avoid
persisting it, as long as it’s okay to recompute the view from the event
log whenever the service restarts.
If you decide you want to present the existing information in a new way,
it is easy to build a new materialized view from the existing event log.
You can also evolve the system to support new features by adding new
types of events, or new properties to existing event types (any older
events remain unmodified). You can also chain new behaviors off
existing events (for example, when a conference attendee cancels, their
seat could be offered to the next person on the waiting list).
If an event was written in error you can delete it again, and then you can
rebuild the views without the deleted event. On the other hand, in a
database where you update and delete data directly, a committed
transaction is often difficult to reverse. Event sourcing can therefore
reduce the number of irreversible actions in the system, making it easier
to change (see “Evolvability: Making Change Easy”).
The event log can also serve as an audit log of everything that happened
in the system, which is valuable in regulated industries that require such
auditability.
You can implement event sourcing on top of any database, but there are also
some systems that are specifically designed to support this pattern, such as
EventStoreDB, MartenDB (based on PostgreSQL), and Axon Framework.
You can also use message brokers such as Apache Kafka to store the event
log, and stream processors can keep the materialized views up-to-date; we
will return to these topics in [Link to Come].
The only important requirement is that the event storage system must
guarantee that all materialized views process the events in exactly the same
order as they appear in the log; as we shall see in [Link to Come], this is not
always easy to achieve in a distributed system.
Dataframes, Matrices, and Arrays
The data models we have seen so far in this chapter are generally used for
both transaction processing and analytics purposes (see “Transaction
Processing versus Analytics”). There are also some data models that you are
likely to encounter in an analytical or scientific context, but that rarely
feature in OLTP systems: dataframes and multidimensional arrays of
numbers such as matrices.
Dataframe APIs also offer a wide variety of operations that go far beyond
what relational databases offer, and the data model is often used in ways
that are very different from typical relational data modelling [64]. For
example, a common use of dataframes is to transform data from a
relational-like representation into a matrix or multidimensional array
representation, which is the form that many machine learning algorithms
expect of their input.
A matrix can only contain numbers, and various techniques are used to
transform non-numerical data into numbers in the matrix. For example:
Dates (which are omitted from the example matrix in Figure 3-9) could
be scaled to be floating-point numbers within some suitable range.
For columns that can only take one of a small, fixed set of values (for
example, the genre of a movie in a database of movies), a one-hot
encoding is often used: we create a column for each possible value (one
for “comedy”, one for “drama”, one for “horror”, etc.), and for each row
representing a movie, we put a 1 in the column corresponding to the
genre of that movie, and a 0 in all the other columns. This representation
also easily generalizes to movies that fit within several genres.
There are also databases such as TileDB [65] that specialize in storing large
multidimensional arrays of numbers; they are called array databases and
are most commonly used for scientific datasets such as geospatial
measurements (raster data on a regularly spaced grid), medical imaging, or
observations from astronomical telescopes [66]. Dataframes are also used in
the financial industry for representing time series data, such as the prices of
assets and trades over time [67].
Summary
Data models are a huge subject, and in this chapter we have taken a quick
look at a broad variety of different models. We didn’t have space to go into
all the details of each model, but hopefully the overview has been enough to
whet your appetite to find out more about the model that best fits your
application’s requirements.
The relational model, despite being more than half a century old, remains
an important data model for many applications—especially in data
warehousing and business analytics, where relational star or snowflake
schemas and SQL queries are ubiquitous. However, several alternatives to
relational data have also become popular in other domains:
The document model targets use cases where data comes in self-
contained JSON documents, and where relationships between one
document and another are rare.
Graph data models go in the opposite direction, targeting use cases
where anything is potentially related to everything, and where queries
potentially need to traverse multiple hops to find the data of interest
(which can be expressed using recursive queries in Cypher, SPARQL, or
Datalog).
Dataframes generalize relational data to large numbers of columns, and
thereby provide a bridge between databases and the multidimensional
arrays that form the basis of much machine learning, statistical data
analysis, and scientific computing.
Various specialist databases have therefore been developed for each data
model, providing query languages and storage engines that are optimized
for a particular model. However, there is also a trend for databases to
expand into neighboring niches by adding support for other data models: for
example, relational databases have added support for document data in the
form of JSON columns, document databases have added relational-like
joins, and support for graph data within SQL is gradually improving.
One thing that non-relational data models have in common is that they
typically don’t enforce a schema for the data they store, which can make it
easier to adapt applications to changing requirements. However, your
application most likely still assumes that data has a certain structure; it’s
just a question of whether the schema is explicit (enforced on write) or
implicit (assumed on read).
Although we have covered a lot of ground, there are still data models left
unmentioned. To give just a few brief examples:
We have to leave it there for now. In the next chapter we will discuss some
of the trade-offs that come into play when implementing the data models
described in this chapter.
FOOTNOTES
REFERENCES
amie Brandon. Unexplanations: query optimization works because sql is declarative. scattered-
thoughts.net, February 2024. Archived at perma.cc/P6W2-WMFZ
oseph M. Hellerstein. The Declarative Imperative: Experiences and Conjectures in Distributed Logic.
Tech report UCB/EECS-2010-90, Electrical Engineering and Computer Sciences, University of
California at Berkeley, June 2010. Archived at perma.cc/K56R-VVQM
Edgar F. Codd. A Relational Model of Data for Large Shared Data Banks. Communications of the
ACM, volume 13, issue 6, pages 377–387, June 1970. doi:10.1145/362384.362685
Michael Stonebraker and Joseph M. Hellerstein. What Goes Around Comes Around. In Readings in
Database Systems, 4th edition, MIT Press, pages 2–41, 2005. ISBN: 9780262693141
Vlad Mihalcea. N+1 query problem with JPA and Hibernate. vladmihalcea.com, January 2023.
Archived at perma.cc/79EV-TZKB
ens Schauder. This is the Beginning of the End of the N+1 Problem: Introducing Single Query
Loading. spring.io, August 2023. Archived at perma.cc/6V96-R333
William Zola. 6 Rules of Thumb for MongoDB Schema Design. mongodb.com, June 2014. Archived at
perma.cc/T2BZ-PPJB
Sidney Andrews and Christopher McClister. Data modeling in Azure Cosmos DB.
learn.microsoft.com, February 2023. Archived at archive.org
Raffi Krikorian. Timelines at Scale. At QCon San Francisco, November 2012. Archived at
perma.cc/V9G5-KLYK
Ralph Kimball and Margy Ross. The Data Warehouse Toolkit: The Definitive Guide to Dimensional
Modeling, 3rd edition. John Wiley & Sons, July 2013. ISBN: 9781118530801
Michael Kaminsky. Data warehouse modeling: Star schema vs. OBT. fivetran.com, August 2022.
Archived at perma.cc/2PZK-BFFP
Joe Nelson. User-defined Order in SQL. begriffs.com, March 2018. Archived at perma.cc/GS3W-
F7AD
Evan Wallace. Realtime Editing of Ordered Sequences. figma.com, March 2017. Archived at
perma.cc/K6ER-CQZW
Amr Awadallah. Schema-on-Read vs. Schema-on-Write. At Berkeley EECS RAD Lab Retreat, Santa
Cruz, CA, May 2009. Archived at perma.cc/DTB2-JCFR
Martin Odersky. The Trouble with Types. At Strange Loop, September 2013. Archived at
perma.cc/85QE-PVEP
Shlomi Noach. gh-ost: GitHub’s Online Schema Migration Tool for MySQL. github.blog, August
2016. Archived at perma.cc/7XAG-XB72
Shayon Mukherjee. pg-osc: Zero downtime schema changes in PostgreSQL. shayon.dev, February
2022. Archived at perma.cc/35WN-7WMY
Carlos Pérez-Aradros Herce. Introducing pgroll: zero-downtime, reversible, schema migrations for
Postgres. xata.io, October 2023. Archived at archive.org
James C. Corbett, Jeffrey Dean, Michael Epstein, Andrew Fikes, Christopher Frost, JJ Furman, Sanjay
Ghemawat, Andrey Gubarev, Christopher Heiser, Peter Hochschild, Wilson Hsieh, Sebastian
Kanthak, Eugene Kogan, Hongyi Li, Alexander Lloyd, Sergey Melnik, David Mwaura, David Nagle,
Sean Quinlan, Rajesh Rao, Lindsay Rolig, Dale Woodford, Yasushi Saito, Christopher Taylor, Michal
Szymaniak, and Ruth Wang. Spanner: Google’s Globally-Distributed Database. At 10th USENIX
Symposium on Operating System Design and Implementation (OSDI), October 2012.
Donald K. Burleson. Reduce I/O with Oracle Cluster Tables. dba-oracle.com. Archived at
perma.cc/7LBJ-9X2C
Fay Chang, Jeffrey Dean, Sanjay Ghemawat, Wilson C. Hsieh, Deborah A. Wallach, Mike Burrows,
Tushar Chandra, Andrew Fikes, and Robert E. Gruber. Bigtable: A Distributed Storage System for
Structured Data. At 7th USENIX Symposium on Operating System Design and Implementation
(OSDI), November 2006.
Priscilla Walmsley. XQuery, 2nd Edition. O’Reilly Media, December 2015. ISBN: 9781491915080
Paul C. Bryan, Kris Zyp, and Mark Nottingham. JavaScript Object Notation (JSON) Pointer. RFC
6901, IETF, April 2013.
Stefan Gössner, Glyn Normington, and Carsten Bormann. JSONPath: Query Expressions for JSON.
RFC 9535, IETF, February 2024.
Lawrence Page, Sergey Brin, Rajeev Motwani, and Terry Winograd. The PageRank Citation Ranking:
Bringing Order to the Web. Technical Report 1999-66, Stanford University InfoLab, November 1999.
Archived at perma.cc/UML9-UZHW
Nathan Bronson, Zach Amsden, George Cabrera, Prasad Chakka, Peter Dimov, Hui Ding, Jack Ferris,
Anthony Giardullo, Sachin Kulkarni, Harry Li, Mark Marchukov, Dmitri Petrov, Lovro Puzar, Yee
Jiun Song, and Venkat Venkataramani. TAO: Facebook’s Distributed Data Store for the Social Graph.
At USENIX Annual Technical Conference (ATC), June 2013.
Natasha Noy, Yuqing Gao, Anshu Jain, Anant Narayanan, Alan Patterson, and Jamie Taylor. Industry-
Scale Knowledge Graphs: Lessons and Challenges. Communications of the ACM, volume 62, issue 8,
pages 36–43, August 2019. doi:10.1145/3331166
Xiyang Feng, Guodong Jin, Ziyi Chen, Chang Liu, and Semih Salihoğlu. KÙZU Graph Database
Management System. At 3th Annual Conference on Innovative Data Systems Research (CIDR 2023),
January 2023.
Maciej Besta, Emanuel Peter, Robert Gerstenberger, Marc Fischer, Michał Podstawski, Claude
Barthels, Gustavo Alonso, Torsten Hoefler. Demystifying Graph Databases: Analysis and Taxonomy
of Data Organization, System Designs, and Graph Queries. arxiv.org, October 2019.
Nadime Francis, Alastair Green, Paolo Guagliardo, Leonid Libkin, Tobias Lindaaker, Victor Marsault,
Stefan Plantikow, Mats Rydberg, Petra Selmer, and Andrés Taylor. Cypher: An Evolving Query
Language for Property Graphs. At International Conference on Management of Data (SIGMOD),
pages 1433–1445, May 2018. doi:10.1145/3183713.3190657
Francesco Tisiot. Explore the new SEARCH and CYCLE features in PostgreSQL® 14. aiven.io,
December 2021. Archived at perma.cc/J6BT-83UZ
Alin Deutsch, Nadime Francis, Alastair Green, Keith Hare, Bei Li, Leonid Libkin, Tobias Lindaaker,
Victor Marsault, Wim Martens, Jan Michels, Filip Murlak, Stefan Plantikow, Petra Selmer, Oskar van
Rest, Hannes Voigt, Domagoj Vrgoč, Mingxi Wu, and Fred Zemke. Graph Pattern Matching in GQL
and SQL/PGQ. At International Conference on Management of Data (SIGMOD), pages 2246–2258,
June 2022. doi:10.1145/3514221.3526057
Alastair Green. SQL... and now GQL. opencypher.org, September 2019. Archived at perma.cc/AFB2-
3SY7
Alin Deutsch, Yu Xu, and Mingxi Wu. Seamless Syntactic and Semantic Integration of Query
Primitives over Relational and Graph Data in GSQL. tigergraph.com, November 2018. Archived at
perma.cc/JG7J-Y35X
Oskar van Rest, Sungpack Hong, Jinha Kim, Xuming Meng, and Hassan Chafi. PGQL: a property
graph query language. At 4th International Workshop on Graph Data Management Experiences and
Systems (GRADES), June 2016. doi:10.1145/2960414.2960421
Amazon Web Services. Neptune Graph Data Model. Amazon Neptune User Guide,
docs.aws.amazon.com. Archived at perma.cc/CX3T-EZU9
David Beckett and Tim Berners-Lee. Turtle – Terse RDF Triple Language. W3C Team Submission,
March 2011.
Sinclair Target. Whatever Happened to the Semantic Web? twobithistory.org, May 2018. Archived at
perma.cc/M8GL-9KHS
Gavin Mendel-Gleason. The Semantic Web is Dead – Long Live the Semantic Web! terminusdb.com,
August 2022. Archived at perma.cc/G2MZ-DSS3
Manu Sporny. JSON-LD and Why I Hate the Semantic Web. manu.sporny.org, January 2014.
Archived at perma.cc/7PT4-PJKF
University of Michigan Library. Biomedical Ontologies and Controlled Vocabularies,
guides.lib.umich.edu/ontology. Archived at perma.cc/Q5GA-F2N8
Matt Haughey. Everything you ever wanted to know about unfurling but were afraid to ask /or/ How
to make your site previews look amazing in Slack. medium.com, November 2015. Archived at
perma.cc/C7S8-4PZN
W3C RDF Working Group. Resource Description Framework (RDF). w3.org, February 2004.
Steve Harris, Andy Seaborne, and Eric Prud’hommeaux. SPARQL 1.1 Query Language. W3C
Recommendation, March 2013.
Todd J. Green, Shan Shan Huang, Boon Thau Loo, and Wenchao Zhou. Datalog and Recursive Query
Processing. Foundations and Trends in Databases, volume 5, issue 2, pages 105–195, November
2013. doi:10.1561/1900000017
Stefano Ceri, Georg Gottlob, and Letizia Tanca. What You Always Wanted to Know About Datalog
(And Never Dared to Ask). IEEE Transactions on Knowledge and Data Engineering, volume 1, issue
1, pages 146–166, March 1989. doi:10.1109/69.43410
Serge Abiteboul, Richard Hull, and Victor Vianu. Foundations of Databases. Addison-Wesley, 1995.
ISBN: 9780201537710, available online at webdam.inria.fr/Alice
Scott Meyer, Andrew Carter, and Andrew Rodriguez. LIquid: The soul of a new graph database, Part
2. engineering.linkedin.com, September 2020. Archived at perma.cc/K9M4-PD6Q
Matt Bessey. Why, after 6 years, I’m over GraphQL. bessey.dev, May 2024. Archived at
perma.cc/2PAU-JYRA
Dominic Betts, Julián Domínguez, Grigori Melnik, Fernando Simonazzi, and Mani Subramanian.
Exploring CQRS and Event Sourcing. Microsoft Patterns & Practices, July 2012. ISBN: 1621140164,
archived at perma.cc/7A39-3NM8
Greg Young. CQRS and Event Sourcing. At Code on the Beach, August 2014.
Devin Petersohn, Stephen Macke, Doris Xin, William Ma, Doris Lee, Xiangxi Mo, Joseph E.
Gonzalez, Joseph M. Hellerstein, Anthony D. Joseph, and Aditya Parameswaran. Towards Scalable
Dataframe Systems. Proceedings of the VLDB Endowment, volume 13, issue 11, pages 2033–2046.
doi:10.14778/3407790.3407807
Stavros Papadopoulos, Kushal Datta, Samuel Madden, and Timothy Mattson. The TileDB Array Data
Storage Manager. Proceedings of the VLDB Endowment, volume 10, issue 4, pages 349–360,
November 2016. doi:10.14778/3025111.3025117
Florin Rusu. Multidimensional Array Data Management. Foundations and Trends in Databases,
volume 12, numbers 2–3, pages 69–220, February 2023. doi:10.1561/1900000069
Ed Targett. Bloomberg, Man Group team up to develop open source “ArcticDB” database.
thestack.technology, March 2023. Archived at perma.cc/M5YD-QQYV
Dennis A. Benson, Ilene Karsch-Mizrachi, David J. Lipman, James Ostell, and David L. Wheeler.
GenBank. Nucleic Acids Research, volume 36, database issue, pages D25–D30, December 2007.
doi:10.1093/nar/gkm929
About the Author
Martin Kleppmann is a researcher in distributed systems at the University
of Cambridge, UK. Previously he was a software engineer and entrepreneur
at internet companies including LinkedIn and Rapportive, where he worked
on large-scale data infrastructure. In the process he learned a few things the
hard way, and he hopes this book will save you from repeating the same
mistakes.