Skip to main content

The Dependency Gravity Model

·2705 words·13 mins

Introduction
#

This post is specifically about a planning technique I have used to prioritize my design work effort for many, many years. Defining priorities is one of those things that people seem to have a hard time with. In the interests of efficiency, we want to make sure that we are spending our time on the things that matter the most. I mean, it’s not all that hard to grab a bunch of pieces and glue them all together to build a system that works and claim victory. Unfortunately, one thing I learned very early on in my career is that building it is easy, the real work starts when people start to use it.

The purpose of a model like this is to help determine where to focus resources when building complex systems. There is always a bit of a balancing act as to how much planning needs to go into various parts of a system. The idea here is to build a model you can use to come up with a set of architectural rules that can help guide your team through the system design process.

Key Concepts
#

A few important concepts that we rely on when talking about gravity models

  • interface stability is important, actually, it’s critical. Spending time on the basics and establishing a strong and consistent interface needs to be a priority. You can add stuff later on but, you cannot change the existing interface once it has been established.
  • gravity is about coupling, not importance Gravity is more about system stability and costs, not business priorities (other than the fact that a working system is important to the business). Business logic and frontend design are very important but are also much easier to change than infrastructure choices.
  • this is an engineering prioritization framework As a system designer, you need to keep your internal priorities clear. This does not mean that they are more or less important than the priorities of the business. This model has to be used within the constraints of the business. In fact, many organizations can use a similar model to clarify business priorities as well.
gravity is about coupling, not importance

The Dependency Gravity model is a practical framework for identifying what is hardest to change in a system so you can focus design effort where mistakes are most expensive.

History
#

For as long as computing has existed, people have tried to design efficient systems to handle various types of workloads. This could be anything from airline scheduling and ticketing systems, banking systems, electronic healthcare record systems, research computing, even just plain old websites. My experience mostly revolves around internet based systems so network engineering (from the physical layer up), large UNIX servers running email, name services, storage facilities, computing services, websites, ftp servers, backup servers and yes, I CAN fix your printer (unless you are running Windows).

When I first started building websites (late 1990s), we would procure a server from somewhere - maybe even just build the hardware ourselves from scratch. Then install all the required software like the webserver itself, a relational database like PostgreSQL or maybe MySQL, and probably a bunch of tools to work on the site, monitor loads, etc.

flowchart LR

    internet[The Internet] --- server[Web Service
Database Service
all on one server]
A single server is great for a small site, but once the site starts getting busy, the web and database services will start competing for resources. Sometimes it's not necessarily obvious which service is causing the problem which can make this complicated to debug.

This would work fine until the database started getting larger or the site started getting busier and the whole system started slowing down or worse crashing due to hardware constraints (running out of memory, CPU not powerful enough). Obviously, the easiest solution here was to either buy a bigger server or to buy a second server in order run the database and webserver software on their own servers. Hardware was not that cheap so often the best solution would be to put a light weight (i.e., cheap) webserver in front of the database.

flowchart LR

    internet[The Internet] --- web[Web Service] --- db[Database Service]

Having dedicated hardware for each service makes life easier.

We would keep making improvements like this, like build a load balancer and create a webserver cluster. Unfortunately, adding databases was non-trivial (actually, in some ways this is still true today) so this would often be a bottleneck. Ultimately, it was kind of a whack-a-mole approach to the system. We would figure out what was causing it to slow down and throw another server at it. Images are serving up slowly, let’s throw an image server in there, or maybe we need to add a dedicated search server that indexes the webservers in the middle of the night.

flowchart LR

    internet[The Internet] --> lb[Load Balancer]

    lb --- web1[Web Server]
    lb --- web2[Web Server]
    lb --- web3[Web Server]

    lb --- img[Image Server]
    lb --- search[Search Server]

    web1 --- db[Database]
    web2 --- db
    web3 --- db
    search --- db
    search --- web1

    db --- db2[Backup Database]
At some point it starts becoming a rat's nest and keeping it all straight not only takes a lot of effort but also means that any changes have to thoroughly thought through as one wrong move can clobber the whole system! Consistent interfaces between each service is critical.

Of course, once you start adding more servers you need to write code to connect all of this stuff together. Some of it was easy, like an image server was basically another website just instead of http://www.example.com we would have http://img.example.com . But then, to get images out faster we would write programs that could detect what browser the user was using and how fast their connection was and send smaller or larger images to them. This now meant we would need to write programs that could take a set of image files and generate a bunch of different size images for each one, and keep all that stuff organized.

After a while, we started to get a pretty good idea of how the system would behave under load and started designing our code accordingly. The database was the hardest thing to change. Our attitude was that database tables were forever so by focusing more time on getting the database schema correct and writing database wrappers which could be used from the rest of the code base we were able to create a nice, consistent interface to the database.

The Model
#

So yes, a lot of “trial by fire” type learning going on but as our system matured, so did our processes around how we built the system. As I mentioned earlier, we created a number of layers within the system which helped us make a lot of decisions about how we were designing and prioritizing our coding work.

So how do we apply this madness to modern day systems?

The General Idea
#

In a nutshell, you need to spend more time on the things that are harder to change. Harder to change could mean you actually can’t change it, like an AWS service. It could also mean that a change could have far-reaching consequences, like maybe changing the name of the Users table in the database. Things that are easy to change are things like frontend code (like fixing spelling errors) or business logic.

block
columns 2

    easy["Easier To Change"]
    composition["Frontend code and graphics"]
    arrow2[" "]
    manager["Business Logic\n "]
    arrow4[" "]
    control["Managers\n Interfaces to the lower layers"]
    arrow6[" "]
    providers["Wrappers"]
    hard["Hardest To Change"]
    core["Core\nDatabase / AWS / external APIs"]
    
    
    %% keep columns aligned
    easy --> hard
    
    composition --- manager
    manager --- control
    control --- providers
    providers --- core
    
    %% styling
    style easy fill:#d5f5e3,stroke:#1e8449,stroke-width:2px,color:#000
    style hard fill:#f9ebea,stroke:#c0392b,stroke-width:2px,color:#000
    style composition fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style manager fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style control fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style providers fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style core fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    
    style arrow2 fill:none,stroke:none
    style arrow4 fill:none,stroke:none
    style arrow6 fill:none,stroke:none

The closer you are to the core, the "harder" things are to change and the larger the consequences of making a mistake.

With the dependency gravity model, the things on the bottom are the foundation of the application and rarely change. These are the parts that need to be solid as everything else hinges on it. A bad database schema design can cause serious problems down the road. A poorly designed schema can cause resource issues and slow the database down, the team may also spend a lot of time fixing or working around schema issues, time that could be spent working on something else.

Strategies
#

So, I can already hear it “Perfect is the enemy of done”. This is very true, but it’s not true in every situation. The model is basically telling you how perfect something needs to be. Database/Infrastructure need to be solid, business logic less so, frontend code even less so. The problem though, is that it is not necessarily easy to control some of the applications running at the core level.

Wrappers
#

My preferred way of dealing with this is using wrappers. This is a thin layer of code between a resource and the rest of the system. For example, say you are integrating a third-party API into your system. Instead of just having queries directed at the API all over the codebase, create a wrapper whose sole function is to expose the API to the rest of your code base.

  • If the API changes, you only have a single spot in your codebase to update
  • you can focus on implementing the API correctly in one spot as opposed to lots of different implementations all over the code base
  • you can swap the service for another one in a single spot in the system

An example of a wrapper that can be used for swapping services might one that exposes a function like make_bucket. Initially it could be used to create a bucket on AWS. Later, you could configure the function to make a bucket on Azure…the caller does not know and actually should not care as long as a bucket is created

That last one is a big deal. If you write wrappers in front of a database you can have functions like get_users. Again the caller does not care where the ‘users’ table is and it does not care. The wrapper could be configured to talk to an SQL database, or maybe the SQL database needs to be migrated, or split into two separate databases. The caller does not care where the data is coming from so any core level changes do not affect the code above it

This is all well and good, but it really takes some upfront planning to pull off effortlessly.

Services
#

While wrappers provide a stable interface around a resource, a service provides a stable interface around a capability.

Creating a service is done by splitting the architecture into smaller pieces that all act independently. In the architecture diagrams above, you can see how we separated things like image servers and search servers from the rest of the website workflow. When we did this, we created new services. Each has their own APIs and anything that wants to use the service now needs to go through that API. If you want to upscale/downscale an image you don’t reimplement this in your code, you use the API. If you want to store an image on the image server, you don’t just copy the image to the image server storage area, you use the API.

The end result is that you can do whatever you want behind that API without worrying about breaking the rest of the system. It’s important to keep the API’s interface consistent. You can add stuff but cannot change the existing behavior of the API.

Of course, once you build a service with a nice API, you will want to use wrappers in the rest of the system to connect to this service.

The advantages of a service based architecture are huge. You can swap out the database, write the code in a different language from the rest of your codebase, even move the service to another cloud provider if you want to and the callers would not know or care.

How We Use This In dxaws
#

Individual modules are designed with their own internal dependency gravity model, and the system as a whole follows its own dependency gravity model.

This is what the gravity model looks like within a dxaws module:

block
columns 2
    
    easy["Easier To Change"]
    manager["Manager Layer"]
    arrow4[" "]
    control["Planner / Executor"]
    arrow6[" "]
    providers["Providers / Wrappers"]
    hard["Hardest To Change"]
    core["Core\nAWS / SQL / external APIs"]
    
    
    %% keep columns aligned
    easy --> hard
    
    manager --- control
    control --- providers
    providers --- core
    
    %% styling
    style easy fill:#d5f5e3,stroke:#1e8449,stroke-width:2px,color:#000
    style hard fill:#f9ebea,stroke:#c0392b,stroke-width:2px,color:#000
    style manager fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style control fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style providers fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style core fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    
    style arrow4 fill:none,stroke:none
    style arrow6 fill:none,stroke:none

As we move closer to the core of the system, dependency gravity increases. The closer code is to AWS primitives and external systems, the harder it becomes to change.

spend more time on the things that are hardest to change

In the chart above, the manager is the public interface to the module. To use it, you ask the manager for what you want, for example Please make a bucket for me called this-is-my-test-bucket. The manager then coordinates with the rest of the team to figure out what needs to happen to satisfy this request. The planner and executor perform the work, and the provider is the wrapper interface to AWS.

block
columns 2

    easy["Easier To Change"]
    system["dxaws-system"]
    arrow4[" "]
    composition["Composition Modules"]
    arrow6[" "]
    primitives["Primitive Modules\n dns, s3, acm, cloudfront"]
    hard["Hardest To Change"]
    core["dxaws-core\n base class module"]
    
    
    %% keep columns aligned
    easy --> hard
    
    system --- composition
    composition --- primitives
    primitives --- core
    
    %% styling
    style easy fill:#d5f5e3,stroke:#1e8449,stroke-width:2px,color:#000
    style hard fill:#f9ebea,stroke:#c0392b,stroke-width:2px,color:#000
    style system fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style composition fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style primitives fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    style core fill:#c5dafb,stroke:#9bbff7,stroke-width:2px,color:#000
    
    style arrow4 fill:none,stroke:none
    style arrow6 fill:none,stroke:none

How we use gravity models in the dxaws-* ecosystem. The core and primitive modules are the most critical. Once these are in place and working correctly, the rest of the system becomes a set of building blocks.

In the chart above, you can see how the modules in the ecosystem are prioritized. Core and primitive modules get a lot of attention because they form the foundation of the ecosystem. Composition modules can be much more fluid because they aggregate primitives and build more complex objects.

Conclusion
#

The Dependency Gravity model is not about adding process for the sake of process, nor is it about over-engineering systems that may never grow beyond a simple prototype. It is simply a way of thinking about where mistakes are most costly and where additional design effort is justified.

All systems have constraints, whether they are technical constraints, business constraints, or time constraints. The goal is not to design the perfect system, but to understand which parts of the system need the most careful consideration and which parts can evolve naturally over time. Some parts of a system benefit from experimentation and iteration, while others benefit from stability and predictability.

By recognizing that some components are inherently harder to change than others, we can make better decisions about where to invest effort early in the design process. Stable interfaces, well-defined boundaries, and thoughtful abstractions help reduce the long-term cost of change, allowing systems to evolve without constant refactoring of foundational components.

The Dependency Gravity model provides a simple heuristic:

spend more time on the things that are hardest to change

Everything else becomes easier when the foundation is solid.

In the case of dxaws, this thinking influences how modules are structured, how interfaces are defined, and how responsibilities are separated between layers. Managers provide stable entry points, planners and executors coordinate behavior, and providers isolate the system from external dependencies. The result is a system that can evolve without repeatedly reworking its core assumptions.

Dependency gravity is not limited to infrastructure or cloud systems. The same thinking can be applied to data models, APIs, organizational processes, and even product design. Any system with dependencies will exhibit some form of gravity.

Understanding where that gravity exists helps guide better decisions.