Subscription Pricing Knowledge Base
Model subscription billing platforms as a graph and query implementation complexity.
Introduction
30 Second Summary
Choosing between subscription billing platforms like Stripe, Chargebee, and Recurly means comparing dozens of API objects across three different documentation sites. Figuring out which platform's "Price" maps to another platform's "ItemPrice" or "Plan" is the kind of connected-data problem that spreadsheets handle poorly.
In this project, you will build a Neo4j graph database that models the billing entities of three subscription platforms and answers "which platform is simplest for my pricing page?" with a single query.
What You'll Build
You type a set of pricing page requirements into a Cypher query, hit run, and instantly see which subscription platform needs the fewest objects to implement your design.
By the end of this project, you'll have:
- A queryable knowledge base that ranks Stripe, Chargebee, and Recurly by implementation complexity for any combination of pricing requirements you feed in.
- Cross-platform equivalence mappings that reveal how billing concepts translate across platforms in a single line of Cypher.
- Weighted path traversals that calculate total API objects needed per platform, visualized as a connected graph in Neo4j Browser.
- Secret Mission: Migration path queries that show how to move an existing pricing setup from one platform to another.
Are there any prerequisites?
You should have basic SQL knowledge (familiarity with tables, joins, and queries in MySQL or similar). You also need Homebrew installed on macOS, since that is how you will install Neo4j.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
You're about to model subscription billing entities across three platforms in a graph database. Understanding why this problem is hard in a traditional relational database is the key insight that makes the rest of this project click.
Install and Start Neo4j
To build and query your subscription pricing knowledge base, you need a graph database running locally on your machine.
Neo4j is the graph database you'll use throughout this project. Homebrew handles the entire installation, including the Java 21 runtime that Neo4j depends on.
In this step, get ready to:
- Install Neo4j Community Edition via Homebrew.
- Start Neo4j as a background service.
- Access Neo4j Browser and set your password.
Install Neo4j via Homebrew
Neo4j Community Edition is free and fully sufficient for this project. The Homebrew formula installs everything you need in one command.
- Press Cmd+Space to open your search bar.
- Type Terminal and press Enter to open it.
- Install Neo4j Community Edition by running this command:
brew install neo4j
What does this command do?
Homebrew downloads and installs Neo4j Community Edition along with its required dependency, OpenJDK 21. You do not need to install Java separately.
The installation may take a minute or two depending on your internet speed. You will see Homebrew resolving dependencies and downloading packages in the terminal output.
- Wait for the installation to complete. You should see a summary listing neo4j and openjdk as installed packages.
Seeing an error during install?
- If you see a permission error, try running sudo chown -R $(whoami) /opt/homebrew and then retry the install command.
- If Homebrew cannot find the formula, run brew update first to refresh your package list.
Still stuck? Help me troubleshoot my Neo4j Homebrew installation error.
Start Neo4j and verify it is running
With Neo4j installed, you need to start it as a background service so it stays running while you work.
- Start the Neo4j service by running this command:
brew services start neo4j
What does this command do?
This registers Neo4j as a macOS background service and starts it immediately. The service will also auto-start when your Mac boots up, so you do not need to manually start it each time.
- Confirm the service is running by running:
brew services list
You should see neo4j listed with status started.
Neo4j not showing as started?
- If the status shows error or none, try stopping and restarting with brew services restart neo4j.
- If it still fails, check the Neo4j log for details. The log path is printed in the service output.
Need more help? Help me figure out why Neo4j won't start as a Homebrew service.
Access Neo4j Browser and set your password
Neo4j includes a web-based interface called Neo4j Browser where you will write and run all your Cypher queries throughout this project.
What do you expect to see when you open Neo4j's local web interface for the first time?
- Open your web browser and navigate to http://localhost:7474.
- You will see a connection screen prompting you for credentials.
- Enter neo4j as the username.
- Enter neo4j as the password.
- Click Connect.
Neo4j immediately prompts you to set a new password. The default password must be changed before you can use the database.
- Choose a new password (at least 8 characters) and confirm it.
- Click Change password.
Why does Neo4j force a password change?
Neo4j ships with a well-known default password. Forcing a change on first login prevents accidental exposure if the database were ever reachable on a network. Choose something simple you will remember for the rest of this project.
After setting your password, you land on the Neo4j Browser interface. You will see a welcome page with interactive guides and a Cypher editor at the top of the screen. This editor is where you will type all your queries in the next steps.
Page not loading at localhost:7474?
- Make sure you waited at least 10-15 seconds after starting the service. Neo4j takes a moment to boot up.
- Verify the service is still running with brew services list. If it shows error, restart with brew services restart neo4j.
- Try a hard refresh in your browser with Cmd+Shift+R.
Still can't connect? Help me troubleshoot why Neo4j Browser won't load at localhost:7474.
Your graph database is running and ready to accept queries. Next up, you will model subscription billing platforms as nodes and relationships in your first Cypher queries.
Model and Create the Platform Entity Graph
Your Neo4j instance is running and you have the Browser open. Now you need data to query.
Before you can answer "which platform is simplest?", you need to represent each platform's billing objects as nodes and show how they connect. In this step, you will create the graph structure that models Stripe, Chargebee, and Recurly's billing entities.
In this step, get ready to:
- Create Platform and Entity nodes for all three subscription billing platforms.
- Connect each Platform to its Entity nodes with HAS_ENTITY relationships.
- Visualize and query the graph in Neo4j Browser.
Create Platform and Entity nodes
A property graph stores data as nodes (things) and relationships (connections between things). Each node can have labels (categories) and properties (key-value pairs).
- In the Cypher editor at the top of Neo4j Browser, paste and run this query to create all 18 nodes (3 platforms and 15 entities):
// Create Platforms
CREATE (stripe:Platform {name: 'Stripe'})
CREATE (chargebee:Platform {name: 'Chargebee'})
CREATE (recurly:Platform {name: 'Recurly'})
// Stripe entities
CREATE (s_product:Entity {name: 'Product', platform: 'Stripe', description: 'What you sell - goods or services'})
CREATE (s_price:Entity {name: 'Price', platform: 'Stripe', description: 'How much and how often to charge for a Product'})
CREATE (s_subscription:Entity {name: 'Subscription', platform: 'Stripe', description: 'Customer scheduled recurring purchase'})
CREATE (s_sub_item:Entity {name: 'SubscriptionItem', platform: 'Stripe', description: 'Line item on a subscription referencing a Price'})
CREATE (s_coupon:Entity {name: 'Coupon', platform: 'Stripe', description: 'Discount applied to subscription or invoice'})
// Chargebee entities
CREATE (c_item:Entity {name: 'Item', platform: 'Chargebee', description: 'Catalog entry - plan, addon, or charge'})
CREATE (c_item_price:Entity {name: 'ItemPrice', platform: 'Chargebee', description: 'Price point for an item - currency, model, period'})
CREATE (c_subscription:Entity {name: 'Subscription', platform: 'Chargebee', description: 'Customer commitment to plan and addons'})
CREATE (c_sub_item:Entity {name: 'SubscriptionItem', platform: 'Chargebee', description: 'Individual item price within a subscription'})
CREATE (c_coupon:Entity {name: 'Coupon', platform: 'Chargebee', description: 'Discount applied to subscription'})
// Recurly entities
CREATE (r_plan:Entity {name: 'Plan', platform: 'Recurly', description: 'Recurring billing product with frequency and base price'})
CREATE (r_addon:Entity {name: 'AddOn', platform: 'Recurly', description: 'Additional charge alongside subscription plan'})
CREATE (r_item:Entity {name: 'Item', platform: 'Recurly', description: 'Reusable sellable unit in the item catalog'})
CREATE (r_subscription:Entity {name: 'Subscription', platform: 'Recurly', description: 'Customer enrollment in a plan with add-ons'})
CREATE (r_coupon:Entity {name: 'Coupon', platform: 'Recurly', description: 'Promotional discount on subscription'})
What does this query do?
Each CREATE statement makes a new node. The word before the curly braces (like :Platform or :Entity) is the node's label. The key-value pairs inside the braces are properties.
You are creating two types of nodes: Platform nodes represent each billing provider, and Entity nodes represent the API objects each platform uses (Product, Price, Subscription, etc.).
- Click the blue play button (or press Cmd+Enter on macOS, Ctrl+Enter on Windows) to execute the query.
You should see a status bar at the bottom showing "Added 18 nodes, Set 54 properties, Added 18 labels".
Seeing a syntax error?
Make sure you copied the entire query including all closing parentheses. A missing ) or mismatched quote will cause a parse error.
help me debug a Cypher syntax error in Neo4j Browser
Your nodes exist, but they are not connected yet. Next, you will create relationships that link each Platform to its Entity nodes.
- Connect Stripe to its 5 entities by pasting and running this query:
// Connect Stripe to its entities
MATCH (stripe:Platform {name: 'Stripe'})
MATCH (s_product:Entity {name: 'Product', platform: 'Stripe'})
MATCH (s_price:Entity {name: 'Price', platform: 'Stripe'})
MATCH (s_subscription:Entity {name: 'Subscription', platform: 'Stripe'})
MATCH (s_sub_item:Entity {name: 'SubscriptionItem', platform: 'Stripe'})
MATCH (s_coupon:Entity {name: 'Coupon', platform: 'Stripe'})
CREATE (stripe)-[:HAS_ENTITY]->(s_product)
CREATE (stripe)-[:HAS_ENTITY]->(s_price)
CREATE (stripe)-[:HAS_ENTITY]->(s_subscription)
CREATE (stripe)-[:HAS_ENTITY]->(s_sub_item)
CREATE (stripe)-[:HAS_ENTITY]->(s_coupon)
What is MATCH doing here?
MATCH finds existing nodes by their labels and properties. Once found, CREATE draws a directed relationship (the arrow ->) between them. The relationship type [:HAS_ENTITY] is how you name what the connection means.
You should see "Created 5 relationships" in the status bar.
- Connect Chargebee to its entities by running this query:
// Connect Chargebee to its entities
MATCH (chargebee:Platform {name: 'Chargebee'})
MATCH (c_item:Entity {name: 'Item', platform: 'Chargebee'})
MATCH (c_item_price:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (c_subscription:Entity {name: 'Subscription', platform: 'Chargebee'})
MATCH (c_sub_item:Entity {name: 'SubscriptionItem', platform: 'Chargebee'})
MATCH (c_coupon:Entity {name: 'Coupon', platform: 'Chargebee'})
CREATE (chargebee)-[:HAS_ENTITY]->(c_item)
CREATE (chargebee)-[:HAS_ENTITY]->(c_item_price)
CREATE (chargebee)-[:HAS_ENTITY]->(c_subscription)
CREATE (chargebee)-[:HAS_ENTITY]->(c_sub_item)
CREATE (chargebee)-[:HAS_ENTITY]->(c_coupon)
- Connect Recurly to its entities by running this query:
// Connect Recurly to its entities
MATCH (recurly:Platform {name: 'Recurly'})
MATCH (r_plan:Entity {name: 'Plan', platform: 'Recurly'})
MATCH (r_addon:Entity {name: 'AddOn', platform: 'Recurly'})
MATCH (r_item:Entity {name: 'Item', platform: 'Recurly'})
MATCH (r_subscription:Entity {name: 'Subscription', platform: 'Recurly'})
MATCH (r_coupon:Entity {name: 'Coupon', platform: 'Recurly'})
CREATE (recurly)-[:HAS_ENTITY]->(r_plan)
CREATE (recurly)-[:HAS_ENTITY]->(r_addon)
CREATE (recurly)-[:HAS_ENTITY]->(r_item)
CREATE (recurly)-[:HAS_ENTITY]->(r_subscription)
CREATE (recurly)-[:HAS_ENTITY]->(r_coupon)
After each query you should see "Created 5 relationships". You now have 15 total HAS_ENTITY relationships connecting the 3 platforms to their entities.
Seeing "(no changes, no records)"?
This means MATCH could not find the nodes. Double-check that you ran the first query (the one creating all 18 nodes) successfully. If you need to start over, run MATCH (n) DETACH DELETE n to clear the database, then re-run the node creation query first.
help me fix my Neo4j MATCH query that returns no results
Visualize the graph
With all nodes and relationships in place, you can see the full graph visually. This is already a representation of something that would require a multi-table schema in MySQL.
- Return every node in your database by running this query:
MATCH (n) RETURN n
- Click the graph visualization icon (the circle-and-lines icon on the left side of the result panel) if it is not already selected.
Before you look at the result, predict what you will see. How many hubs should there be, and how many smaller nodes should surround each hub?
You should see 3 Platform hubs, each connected to 5 Entity nodes by HAS_ENTITY arrows. That is 18 nodes and 15 relationships total.
Why this already beats MySQL
In a relational database, you would need a platforms table, an entities table, and a platform_entities junction table to express the same structure. In Neo4j, the relationships are first-class objects you can see and traverse directly.
Query a single platform's entities
The full graph view is useful for a high-level overview. Now try a targeted query that filters to one platform's billing objects.
- Query Stripe's entities by running:
MATCH (p:Platform {name: 'Stripe'})-[:HAS_ENTITY]->(e:Entity)
RETURN e.name, e.description
What does this pattern do?
The pattern (p:Platform {name: 'Stripe'})-[:HAS_ENTITY]->(e:Entity) reads as: "find a Platform node named Stripe, follow its HAS_ENTITY relationships outward, and bind each destination to e." This pattern-matching syntax is the core of Cypher.
You should see a table with 5 rows listing Product, Price, Subscription, SubscriptionItem, and Coupon alongside their descriptions.
Getting 0 results?
Check that Stripe is capitalized exactly as written. Cypher property values are case-sensitive, so 'stripe' will not match a node whose name is 'Stripe'.
help me debug my Cypher MATCH query that returns empty results
✔️ Awesome, I've got everything!
Great. Double check you have 18 nodes and 15 relationships by running MATCH (n) RETURN count(n) (should return 18).
ⓧ I'd like to double check the full code
Here is the complete set of queries you ran in this step. If you need to start fresh, run MATCH (n) DETACH DELETE n first, then re-run the node creation query below, followed by each relationship query.
Node creation query (run first):
// Create Platforms
CREATE (stripe:Platform {name: 'Stripe'})
CREATE (chargebee:Platform {name: 'Chargebee'})
CREATE (recurly:Platform {name: 'Recurly'})
// Stripe entities
CREATE (s_product:Entity {name: 'Product', platform: 'Stripe', description: 'What you sell - goods or services'})
CREATE (s_price:Entity {name: 'Price', platform: 'Stripe', description: 'How much and how often to charge for a Product'})
CREATE (s_subscription:Entity {name: 'Subscription', platform: 'Stripe', description: 'Customer scheduled recurring purchase'})
CREATE (s_sub_item:Entity {name: 'SubscriptionItem', platform: 'Stripe', description: 'Line item on a subscription referencing a Price'})
CREATE (s_coupon:Entity {name: 'Coupon', platform: 'Stripe', description: 'Discount applied to subscription or invoice'})
// Chargebee entities
CREATE (c_item:Entity {name: 'Item', platform: 'Chargebee', description: 'Catalog entry - plan, addon, or charge'})
CREATE (c_item_price:Entity {name: 'ItemPrice', platform: 'Chargebee', description: 'Price point for an item - currency, model, period'})
CREATE (c_subscription:Entity {name: 'Subscription', platform: 'Chargebee', description: 'Customer commitment to plan and addons'})
CREATE (c_sub_item:Entity {name: 'SubscriptionItem', platform: 'Chargebee', description: 'Individual item price within a subscription'})
CREATE (c_coupon:Entity {name: 'Coupon', platform: 'Chargebee', description: 'Discount applied to subscription'})
// Recurly entities
CREATE (r_plan:Entity {name: 'Plan', platform: 'Recurly', description: 'Recurring billing product with frequency and base price'})
CREATE (r_addon:Entity {name: 'AddOn', platform: 'Recurly', description: 'Additional charge alongside subscription plan'})
CREATE (r_item:Entity {name: 'Item', platform: 'Recurly', description: 'Reusable sellable unit in the item catalog'})
CREATE (r_subscription:Entity {name: 'Subscription', platform: 'Recurly', description: 'Customer enrollment in a plan with add-ons'})
CREATE (r_coupon:Entity {name: 'Coupon', platform: 'Recurly', description: 'Promotional discount on subscription'})
Stripe relationships (run second):
MATCH (stripe:Platform {name: 'Stripe'})
MATCH (s_product:Entity {name: 'Product', platform: 'Stripe'})
MATCH (s_price:Entity {name: 'Price', platform: 'Stripe'})
MATCH (s_subscription:Entity {name: 'Subscription', platform: 'Stripe'})
MATCH (s_sub_item:Entity {name: 'SubscriptionItem', platform: 'Stripe'})
MATCH (s_coupon:Entity {name: 'Coupon', platform: 'Stripe'})
CREATE (stripe)-[:HAS_ENTITY]->(s_product)
CREATE (stripe)-[:HAS_ENTITY]->(s_price)
CREATE (stripe)-[:HAS_ENTITY]->(s_subscription)
CREATE (stripe)-[:HAS_ENTITY]->(s_sub_item)
CREATE (stripe)-[:HAS_ENTITY]->(s_coupon)
Chargebee relationships (run third):
MATCH (chargebee:Platform {name: 'Chargebee'})
MATCH (c_item:Entity {name: 'Item', platform: 'Chargebee'})
MATCH (c_item_price:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (c_subscription:Entity {name: 'Subscription', platform: 'Chargebee'})
MATCH (c_sub_item:Entity {name: 'SubscriptionItem', platform: 'Chargebee'})
MATCH (c_coupon:Entity {name: 'Coupon', platform: 'Chargebee'})
CREATE (chargebee)-[:HAS_ENTITY]->(c_item)
CREATE (chargebee)-[:HAS_ENTITY]->(c_item_price)
CREATE (chargebee)-[:HAS_ENTITY]->(c_subscription)
CREATE (chargebee)-[:HAS_ENTITY]->(c_sub_item)
CREATE (chargebee)-[:HAS_ENTITY]->(c_coupon)
Recurly relationships (run fourth):
MATCH (recurly:Platform {name: 'Recurly'})
MATCH (r_plan:Entity {name: 'Plan', platform: 'Recurly'})
MATCH (r_addon:Entity {name: 'AddOn', platform: 'Recurly'})
MATCH (r_item:Entity {name: 'Item', platform: 'Recurly'})
MATCH (r_subscription:Entity {name: 'Subscription', platform: 'Recurly'})
MATCH (r_coupon:Entity {name: 'Coupon', platform: 'Recurly'})
CREATE (recurly)-[:HAS_ENTITY]->(r_plan)
CREATE (recurly)-[:HAS_ENTITY]->(r_addon)
CREATE (recurly)-[:HAS_ENTITY]->(r_item)
CREATE (recurly)-[:HAS_ENTITY]->(r_subscription)
CREATE (recurly)-[:HAS_ENTITY]->(r_coupon)
Your graph now has the foundation: platforms and their billing entities, visually connected. Next up, you will add cross-platform equivalence relationships so the graph can answer "what is Stripe's Price equivalent to on Chargebee?" in a single traversal.
Add Cross-Platform Equivalences
In the previous step, you built a graph that shows what billing entities each platform offers. That's useful for browsing, but it cannot answer the real question: "What is Stripe's Price equivalent to on Chargebee?"
The missing piece is relationships between entities across platforms. Once you add EQUIVALENT_TO relationships, a single Cypher query can traverse from one platform's entity to its counterpart on another platform.
In this step, get ready to:
- Create EQUIVALENT_TO relationships that map billing concepts across Stripe, Chargebee, and Recurly.
- Query cross-platform equivalences in a single line of Cypher.
- Traverse multi-hop paths to discover indirect entity connections.
Create EQUIVALENT_TO relationships
Each subscription platform has its own names for what are essentially the same billing concepts. Stripe calls its catalog entry a "Product" while Chargebee calls it an "Item" and Recurly also calls it an "Item." You need relationships that encode these mappings directly in the graph.
- In the Neo4j Browser Cypher editor, paste and run this query to create the product/catalog equivalences:
// Product/catalog equivalences
MATCH (s:Entity {name: 'Product', platform: 'Stripe'})
MATCH (c:Entity {name: 'Item', platform: 'Chargebee'})
MATCH (r:Entity {name: 'Item', platform: 'Recurly'})
CREATE (s)-[:EQUIVALENT_TO {note: 'Catalog entry for what you sell'}]->(c)
CREATE (s)-[:EQUIVALENT_TO {note: 'Catalog entry for what you sell'}]->(r)
CREATE (c)-[:EQUIVALENT_TO {note: 'Catalog entry for what you sell'}]->(r);
What does this code do?
The MATCH clauses find existing Entity nodes by name and platform. The CREATE lines draw EQUIVALENT_TO arrows between them.
Each relationship carries a note property explaining the shared concept. Three platforms means three pairwise relationships per concept group (A to B, A to C, B to C).
You should see "Created 3 relationships" in the output panel. Now add the pricing equivalences.
- Run this query to map how each platform represents pricing:
// Pricing equivalences
MATCH (s:Entity {name: 'Price', platform: 'Stripe'})
MATCH (c:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (r:Entity {name: 'Plan', platform: 'Recurly'})
CREATE (s)-[:EQUIVALENT_TO {note: 'Defines amount, currency, billing interval'}]->(c)
CREATE (s)-[:EQUIVALENT_TO {note: 'Defines amount, currency, billing interval'}]->(r)
CREATE (c)-[:EQUIVALENT_TO {note: 'Defines amount, currency, billing interval'}]->(r);
Why does Stripe's Price map to Recurly's Plan?
Stripe separates "what you sell" (Product) from "how much and how often" (Price). Recurly merges pricing directly into the Plan itself. They serve the same purpose (defining amount, currency, and billing interval) even though they sit at different levels of abstraction.
Another "Created 3 relationships" confirmation. Two more groups to go.
- Run this query to connect subscription entities:
// Subscription equivalences
MATCH (s:Entity {name: 'Subscription', platform: 'Stripe'})
MATCH (c:Entity {name: 'Subscription', platform: 'Chargebee'})
MATCH (r:Entity {name: 'Subscription', platform: 'Recurly'})
CREATE (s)-[:EQUIVALENT_TO {note: 'Customer recurring billing commitment'}]->(c)
CREATE (s)-[:EQUIVALENT_TO {note: 'Customer recurring billing commitment'}]->(r)
CREATE (c)-[:EQUIVALENT_TO {note: 'Customer recurring billing commitment'}]->(r);
"Created 3 relationships" again. One more.
- Run the final equivalence query for coupon entities:
// Coupon equivalences
MATCH (s:Entity {name: 'Coupon', platform: 'Stripe'})
MATCH (c:Entity {name: 'Coupon', platform: 'Chargebee'})
MATCH (r:Entity {name: 'Coupon', platform: 'Recurly'})
CREATE (s)-[:EQUIVALENT_TO {note: 'Discount mechanism'}]->(c)
CREATE (s)-[:EQUIVALENT_TO {note: 'Discount mechanism'}]->(r)
CREATE (c)-[:EQUIVALENT_TO {note: 'Discount mechanism'}]->(r);
"Created 3 relationships" one final time. You now have 12 EQUIVALENT_TO relationships in the graph (4 concept groups x 3 pairwise connections each).
Seeing "Created 0 relationships"?
This means one of the MATCH clauses found no results. Check that the entity names and platform values match exactly what you created in Step 2 (case-sensitive). For example, ItemPrice must be capitalized exactly as shown.
Help me debug my EQUIVALENT_TO relationships not being created.
Query cross-platform equivalences
The relationships exist. Now prove they work by asking the graph: "What is Stripe's Price equivalent to on other platforms?"
Before you run this query, predict what you expect to see. Which entities on Chargebee and Recurly should come back as equivalents of Stripe's Price?
- Run this equivalence lookup query:
MATCH (e:Entity {name: 'Price', platform: 'Stripe'})-[:EQUIVALENT_TO]->(equiv)
RETURN equiv.name AS equivalent_entity, equiv.platform AS on_platform, equiv.description
You should see two rows: Chargebee's ItemPrice and Recurly's Plan. In one line of Cypher, you answered a question that would require a junction table and self-joins in MySQL.
What makes this query powerful?
The pattern (e)-[:EQUIVALENT_TO]->(equiv) reads like English: "find entities equivalent to e." There are no JOINs, no intermediate tables, no subqueries. The graph structure IS the query path.
In a relational database, you would need an entity_equivalences junction table, foreign keys back to the entities table, and a self-join to traverse it. As the number of platforms grows, the SQL gets exponentially more complex. The Cypher stays the same.
Getting zero results?
Make sure you ran all four equivalence creation queries earlier. You can confirm by running MATCH ()-[r:EQUIVALENT_TO]->() RETURN count(r) which should return 12.
Help me figure out why my equivalence query returns nothing.
Try a two-hop traversal
Single-hop queries are useful, but the real power of a graph database shows up when you traverse multiple hops. You can find all entities connected to a starting point through chains of equivalence.
- Run this variable-length path query to find everything equivalent to Chargebee's Item within 1 to 2 hops:
MATCH (start:Entity {name: 'Item', platform: 'Chargebee'})-[:EQUIVALENT_TO*1..2]-(related)
RETURN DISTINCT related.name, related.platform
What does *1..2 mean?
The *1..2 syntax tells Neo4j to follow the EQUIVALENT_TO relationship 1 to 2 times. This means you get direct equivalents (1 hop) AND equivalents-of-equivalents (2 hops).
In SQL, this would require recursive CTEs or multiple self-joins. In Cypher, it is four characters added to the relationship pattern.
You should see Stripe's Product and Recurly's Item at 1 hop, plus additional entities discovered through 2-hop paths. Click the graph visualization icon to see the traversal paths visually.
Seeing duplicate results?
The DISTINCT keyword should prevent duplicates. If you removed it from the query, the same entity can appear multiple times (once for each path that reaches it). Make sure DISTINCT is in your RETURN clause.
Help me understand why my multi-hop query returns unexpected results.
Your knowledge base can now answer cross-platform mapping questions instantly. Next up, you will add pricing requirements with complexity weights so the graph can actually recommend which platform is simplest for a given pricing page design.
Add Pricing Requirements with Complexity Weights
Your graph can now tell you what each platform offers and how entities map across platforms. The natural next question a client would ask is: "given my pricing page requirements, which platform is the simplest to implement on?"
Your current graph cannot answer that. It has no concept of a pricing page requirement, and no way to express how many API objects each platform needs to fulfill one. In this step, you will add weighted relationships that turn your knowledge base into a decision engine.
In this step, get ready to:
- Create PricingRequirement nodes representing common pricing page building blocks.
- Connect requirements to platform entities with REQUIRES relationships carrying complexity weights.
- Run the Pricing Page Architect query to get a ranked platform recommendation.
Before building anything new, try to answer the simplest-platform question with what you already have.
- In the Neo4j Browser Cypher editor, run this query to count each platform's entities:
MATCH (p:Platform)-[:HAS_ENTITY]->(e:Entity)
RETURN p.name AS platform, count(e) AS entity_count
ORDER BY entity_count ASC
Before you look at the result, predict: will this tell you which platform is simplest for a pricing page with per-seat pricing, a monthly/annual toggle, and an add-on?
Every platform returns 5. The count is identical because your graph only knows that entities exist, not how many objects each requirement actually needs on each platform. That is the gap this step closes.
Create PricingRequirement nodes
Pricing requirements represent the building blocks a client might want on their pricing page. Each one will connect to the platform entities needed to implement it.
- Create all seven requirement nodes by running this query in the Cypher editor:
CREATE (:PricingRequirement {name: 'per_seat', description: 'Charge per user/seat with quantity'})
CREATE (:PricingRequirement {name: 'monthly_annual_toggle', description: 'Offer both monthly and annual billing intervals'})
CREATE (:PricingRequirement {name: 'usage_overage', description: 'Metered usage-based charges beyond a threshold'})
CREATE (:PricingRequirement {name: 'addon', description: 'Optional purchasable add-on alongside base plan'})
CREATE (:PricingRequirement {name: 'free_trial', description: 'Trial period before first charge'})
CREATE (:PricingRequirement {name: 'tiered_pricing', description: 'Price changes based on quantity tiers'})
CREATE (:PricingRequirement {name: 'coupon_discount', description: 'Promotional discount code'});
What does this query do?
Each CREATE line adds a PricingRequirement node with a name (used for querying) and a human-readable description.
These nodes are not yet connected to any platform. They represent abstract requirements that could apply to any billing system.
- Confirm the 7 nodes were created by running:
MATCH (r:PricingRequirement)
RETURN r.name, r.description
You should see a table with all seven requirements listed.
Seeing 0 rows?
Make sure you ran the CREATE query first. If you see a syntax error, check that every line begins with CREATE and that all property values use single quotes.
help me debug my PricingRequirement CREATE query in Neo4j
Connect requirements to platform entities with complexity weights
The objects_needed property on each REQUIRES relationship represents how many API objects you must create on that platform to fulfill the requirement. This is the weight that powers your recommendations.
- Run the per-seat pricing relationships first:
// Per-seat pricing
MATCH (req:PricingRequirement {name: 'per_seat'})
MATCH (s_product:Entity {name: 'Product', platform: 'Stripe'})
MATCH (s_price:Entity {name: 'Price', platform: 'Stripe'})
MATCH (c_item:Entity {name: 'Item', platform: 'Chargebee'})
MATCH (c_ip:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (r_plan:Entity {name: 'Plan', platform: 'Recurly'})
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Price with per_unit billing_scheme and quantity'}]->(s_price)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Product to attach the price to'}]->(s_product)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Item with type plan'}]->(c_item)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'ItemPrice with per_unit pricing_model'}]->(c_ip)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Plan with per_unit pricing_model'}]->(r_plan);
What does this pattern do?
The MATCH lines find the requirement node and the specific platform entities it depends on. The CREATE lines draw REQUIRES relationships from the requirement to each entity.
The objects_needed property is the key insight. Per-seat pricing on Stripe requires 1 Product AND 1 Price (total 2 objects). On Chargebee it requires 1 Item AND 1 ItemPrice (total 2). On Recurly it requires just 1 Plan (total 1). These weights are what let the graph rank platforms by implementation complexity.
Neo4j Browser shows "Created 5 relationships, set 10 properties" confirming the query worked.
- Run the monthly/annual toggle relationships:
// Monthly/annual toggle
MATCH (req:PricingRequirement {name: 'monthly_annual_toggle'})
MATCH (s_price:Entity {name: 'Price', platform: 'Stripe'})
MATCH (c_ip:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (r_plan:Entity {name: 'Plan', platform: 'Recurly'})
CREATE (req)-[:REQUIRES {objects_needed: 2, note: 'Two Price objects: one monthly, one annual'}]->(s_price)
CREATE (req)-[:REQUIRES {objects_needed: 2, note: 'Two ItemPrices: one monthly, one annual'}]->(c_ip)
CREATE (req)-[:REQUIRES {objects_needed: 2, note: 'Two Plans: one monthly, one annual'}]->(r_plan);
Why objects_needed is 2 here
A monthly/annual toggle needs two separate billing objects on every platform: one for monthly, one for annual. The weight of 2 reflects this. Some requirements need more objects than others, and that difference is what makes one platform simpler than another for a given set of needs.
- Run the usage-based overage relationships:
// Usage-based overage
MATCH (req:PricingRequirement {name: 'usage_overage'})
MATCH (s_price:Entity {name: 'Price', platform: 'Stripe'})
MATCH (c_item:Entity {name: 'Item', platform: 'Chargebee'})
MATCH (c_ip:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (r_addon:Entity {name: 'AddOn', platform: 'Recurly'})
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Metered Price with usage_type=metered'}]->(s_price)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Item with metered=true'}]->(c_item)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'ItemPrice for metered item'}]->(c_ip)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Usage-based add-on with billing in arrears'}]->(r_addon);
- Run the add-on relationships:
// Add-on
MATCH (req:PricingRequirement {name: 'addon'})
MATCH (s_product:Entity {name: 'Product', platform: 'Stripe'})
MATCH (s_price:Entity {name: 'Price', platform: 'Stripe'})
MATCH (c_item:Entity {name: 'Item', platform: 'Chargebee'})
MATCH (c_ip:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (r_addon:Entity {name: 'AddOn', platform: 'Recurly'})
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Separate Product for the add-on'}]->(s_product)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Recurring Price for the add-on product'}]->(s_price)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Item with type addon'}]->(c_item)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'ItemPrice for addon item'}]->(c_ip)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Plan-level or item-based add-on'}]->(r_addon);
- Confirm your progress so far by counting the REQUIRES relationships:
MATCH ()-[r:REQUIRES]->()
RETURN count(r) AS total_requires_relationships
You should see 17. Four requirements connected, three more to go.
- Run the free trial relationships:
// Free trial
MATCH (req:PricingRequirement {name: 'free_trial'})
MATCH (s_price:Entity {name: 'Price', platform: 'Stripe'})
MATCH (c_ip:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (r_plan:Entity {name: 'Plan', platform: 'Recurly'})
CREATE (req)-[:REQUIRES {objects_needed: 0, note: 'trial_period_days on Price - no extra object'}]->(s_price)
CREATE (req)-[:REQUIRES {objects_needed: 0, note: 'trial_period on ItemPrice - no extra object'}]->(c_ip)
CREATE (req)-[:REQUIRES {objects_needed: 0, note: 'trial_interval on Plan - no extra object'}]->(r_plan);
Why objects_needed is 0 here
Free trials are a property on existing pricing objects, not separate objects. On Stripe, you set trial_period_days on the Price. On Chargebee, trial_period on ItemPrice. On Recurly, trial_interval on Plan. Zero extra objects needed.
This is powerful information for a consultant. Free trials add no implementation complexity on any platform.
- Run the tiered pricing relationships:
// Tiered pricing
MATCH (req:PricingRequirement {name: 'tiered_pricing'})
MATCH (s_price:Entity {name: 'Price', platform: 'Stripe'})
MATCH (c_ip:Entity {name: 'ItemPrice', platform: 'Chargebee'})
MATCH (r_addon:Entity {name: 'AddOn', platform: 'Recurly'})
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Price with billing_scheme=tiered and tiers array'}]->(s_price)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'ItemPrice with pricing_model=tiered'}]->(c_ip)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Add-on with tiered pricing model'}]->(r_addon);
- Run the coupon discount relationships:
// Coupon discount
MATCH (req:PricingRequirement {name: 'coupon_discount'})
MATCH (s_coupon:Entity {name: 'Coupon', platform: 'Stripe'})
MATCH (c_coupon:Entity {name: 'Coupon', platform: 'Chargebee'})
MATCH (r_coupon:Entity {name: 'Coupon', platform: 'Recurly'})
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Coupon object with percent_off or amount_off'}]->(s_coupon)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Coupon with discount type'}]->(c_coupon)
CREATE (req)-[:REQUIRES {objects_needed: 1, note: 'Coupon with discount'}]->(r_coupon);
- Verify the total REQUIRES relationships by running:
MATCH ()-[r:REQUIRES]->()
RETURN count(r) AS total_requires_relationships
You should see 22. All seven requirements are now wired to their platform entities with complexity weights.
Seeing fewer than 22 relationships?
If a query created 0 relationships, it means a MATCH clause found no matching node. Double-check that the entity names and platform values in the MATCH lines exactly match what you created in Step 2 (case-sensitive).
Run MATCH (e:Entity) RETURN e.name, e.platform to see all entity names and compare them against the MATCH clauses in the failing query.
help me find why my REQUIRES relationships are missing in Neo4j
✔️ Awesome, I've got everything!
Great. All 7 PricingRequirement nodes and 22 REQUIRES relationships are in place. Time to run the query that makes it all worthwhile.
ⓧ I'd like to double check the full code
Scroll up and confirm you ran each of these blocks in order:
- The 7-line PricingRequirement CREATE block (03-create-requirements.cypher).
- Per-seat pricing REQUIRES block (5 relationships).
- Monthly/annual toggle REQUIRES block (3 relationships).
- Usage-based overage REQUIRES block (4 relationships).
- Add-on REQUIRES block (5 relationships).
- Free trial REQUIRES block (3 relationships).
- Tiered pricing REQUIRES block (3 relationships).
- Coupon discount REQUIRES block (3 relationships).
If your count query returns 22, you are good to go.
Run the Pricing Page Architect query
This is the payoff. You will feed a set of pricing page requirements into the graph, and it will traverse REQUIRES relationships, sum the objects_needed weights per platform, and return a ranked recommendation.
Before you run the next query, predict: given per-seat pricing, a monthly/annual toggle, an add-on, and a free trial, which platform do you think needs the fewest objects?
- Run the Pricing Page Architect query:
WITH ['per_seat', 'monthly_annual_toggle', 'addon', 'free_trial'] AS requirements
MATCH (req:PricingRequirement)-[r:REQUIRES]->(e:Entity)
WHERE req.name IN requirements
MATCH (p:Platform)-[:HAS_ENTITY]->(e)
RETURN p.name AS platform,
sum(r.objects_needed) AS total_objects_needed,
collect(DISTINCT e.name) AS entities_involved,
collect(DISTINCT req.name) AS requirements_fulfilled
ORDER BY total_objects_needed ASC
What does this query do?
The WITH clause defines your requirements list. MATCH finds every REQUIRES path from those requirements to platform entities. sum(r.objects_needed) totals the complexity weight per platform.
The platform at the top of the result needs the fewest API objects. Change the requirements list and rerun to instantly get a new recommendation for any client scenario.
You should see a ranked table with three rows. The platform at the top is your recommendation for this set of requirements. Notice how free_trial contributes 0 objects on every platform, so it does not change the ranking.
- Try changing the requirements to see how the ranking shifts. Replace the requirements list with a different combination:
WITH ['per_seat', 'usage_overage', 'tiered_pricing', 'coupon_discount'] AS requirements
MATCH (req:PricingRequirement)-[r:REQUIRES]->(e:Entity)
WHERE req.name IN requirements
MATCH (p:Platform)-[:HAS_ENTITY]->(e)
RETURN p.name AS platform,
sum(r.objects_needed) AS total_objects_needed,
collect(DISTINCT e.name) AS entities_involved,
collect(DISTINCT req.name) AS requirements_fulfilled
ORDER BY total_objects_needed ASC
The ranking changes depending on which requirements you feed in. This is the core value of the knowledge base: a single query that answers a real consulting question.
Seeing NULL or 0 for all platforms?
If all total_objects_needed values are 0 or NULL, your REQUIRES relationships may not have been created. Run MATCH ()-[r:REQUIRES]->() RETURN count(r) to check. If it returns 0, scroll back up and re-run the relationship queries from substep 2.
help me debug my Pricing Page Architect query returning empty results
Your knowledge base now answers the core consulting question in a single Cypher query. Next up, you will build drill-down queries that show the exact entity-by-entity implementation path per platform.
Query the Full Implementation Path
Your Pricing Page Architect query now ranks platforms by total object count. That number is useful for a quick recommendation, but a consultant needs more.
A client wants to see the exact entity-by-entity path: "For per-seat pricing on Stripe, you create a Product, then a Price with billing_scheme=per_unit." This step builds three drill-down queries that turn your graph into a complete implementation guide.
In this step, get ready to:
- Build a detailed implementation path query that breaks down each requirement into platform-specific entities and setup notes.
- Discover which features are native to each platform (zero extra objects needed).
- Compare two platforms side-by-side for a specific requirement.
Build the detailed implementation path query
The aggregate query from the previous step tells you the total count. This query unpacks that count into individual rows showing exactly which entity each requirement touches on each platform.
- Paste and run the following query in the Cypher editor:
// DETAILED IMPLEMENTATION PATH
WITH ['per_seat', 'monthly_annual_toggle', 'usage_overage', 'addon'] AS requirements
MATCH (req:PricingRequirement)-[r:REQUIRES]->(e:Entity)
WHERE req.name IN requirements
MATCH (p:Platform)-[:HAS_ENTITY]->(e)
RETURN req.name AS requirement,
p.name AS platform,
e.name AS entity,
r.objects_needed AS objects,
r.note AS implementation_note
ORDER BY req.name, p.name;
What does this query do?
Instead of summing objects per platform, this query returns one row per requirement-platform-entity combination.
- The WITH clause defines which requirements you care about (swap these to model any client scenario).
- The implementation_note column gives you the exact API detail (e.g., "Price with per_unit billing_scheme and quantity").
- Ordering by req.name, p.name groups the results so you can compare platforms requirement-by-requirement.
You should see a table with rows for each requirement broken out by platform. For example, the per_seat requirement shows that Stripe needs a Product and a Price, Chargebee needs an Item and an ItemPrice, and Recurly needs just a Plan.
Why is this valuable for a consultant?
A consultant can now say: "On Stripe, per-seat pricing needs a Product and a Price with per_unit billing_scheme. On Chargebee, it needs an Item and an ItemPrice with per_unit pricing_model. On Recurly, it just needs a Plan with per_unit pricing." That level of specificity closes deals.
Seeing no results?
Make sure the requirement names in the WITH array match exactly what you created earlier (e.g., per_seat not perSeat). Names are case-sensitive in Neo4j.
Help me debug my Cypher query that returns no rows.
Find requirements a platform handles natively
Some features cost zero extra objects because they are built into an existing entity as a property. Knowing which features are "free" on each platform is a major selling point in a recommendation.
- Run the following query to find all requirements with objects_needed = 0:
// NATIVE FEATURES (zero extra objects)
MATCH (req:PricingRequirement)-[r:REQUIRES]->(e:Entity)
WHERE r.objects_needed = 0
MATCH (p:Platform)-[:HAS_ENTITY]->(e)
RETURN p.name AS platform, req.name AS requirement, r.note AS why_its_free;
What does this reveal?
This query filters for REQUIRES relationships where the weight is zero. These are features baked into existing entities as properties rather than separate API objects.
For example, free_trial shows up for all three platforms because trials are just a property on the pricing entity (trial_period_days on Stripe's Price, trial_period on Chargebee's ItemPrice, trial_interval on Recurly's Plan).
You should see rows for free_trial on all three platforms, each with a note explaining why no extra object is needed.
Compare two platforms side-by-side for a specific requirement
Sometimes a client has narrowed their choice to two platforms. This query gives a focused comparison for a single requirement.
- Run the side-by-side comparison for usage_overage on Stripe vs Chargebee:
// SIDE-BY-SIDE COMPARISON
MATCH (req:PricingRequirement {name: 'usage_overage'})-[r:REQUIRES]->(e:Entity)
MATCH (p:Platform)-[:HAS_ENTITY]->(e)
WHERE p.name IN ['Stripe', 'Chargebee']
RETURN p.name AS platform, e.name AS entity_needed, r.objects_needed, r.note
ORDER BY p.name;
What does this query do?
- The WHERE p.name IN ['Stripe', 'Chargebee'] clause limits results to just two platforms for a focused comparison.
- Swap in any two platform names and any requirement name to compare any combination.
- This is the consultant's "final two candidates" view for a specific feature.
Before you check the results, what do you think you will see? Will both platforms need the same number of entities, or will one need more?
You should see that Stripe needs 1 entity (a metered Price) while Chargebee needs 2 entities (an Item with metered=true plus an ItemPrice for that item). That difference matters when a client asks "which is simpler for usage billing?"
Results look incomplete?
Check that the platform names in the WHERE clause use title case (Stripe not stripe). Property values in Neo4j are case-sensitive.
Help me troubleshoot my side-by-side Cypher comparison query.
You now have a full consulting toolkit: a ranked recommendation, a detailed implementation path, a native-feature finder, and a side-by-side comparison. Next up, you will extend this into a Secret Mission that adds platform migration paths.
Secret mission
Add Platform Migration Paths
Your knowledge base can answer "which platform should I choose?" but what about the follow-up: "how do I migrate FROM my current platform TO the one you recommended?" Extend your graph with migration complexity scores and entity-by-entity migration path queries.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Neo4j Community Edition, so there are no ongoing costs. Decide whether to keep your Neo4j service running, pause it to come back later, or delete it entirely.
Resources you used:
- Neo4j Community Edition running as a macOS Homebrew service (free, no cost).
- Graph data (18+ nodes, 40+ relationships) stored locally in the Homebrew cellar path.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend your knowledge base with new platforms and requirements.
- Neo4j will auto-start on boot as a Homebrew service. It uses minimal resources when idle.
- Your graph data persists across restarts. You can open Neo4j Browser at http://localhost:7474 any time to query your knowledge base.
Pause - I'll come back to this later
Stop the Neo4j service to free up memory. Your graph data stays on disk and will be ready when you restart.
- Stop the Neo4j service by running:
brew services stop neo4j
- When you are ready to return, start it again by running:
brew services start neo4j
Delete - I don't want to use this again
Remove Neo4j completely from your machine. This deletes the service, the binary, and all graph data stored in the Homebrew cellar.
- Stop the Neo4j service and uninstall it by running:
brew services stop neo4j && brew uninstall neo4j
- Confirm Neo4j is removed by running:
brew list | grep neo4j
- You should see no output, confirming Neo4j is fully uninstalled.
Nice Work!
Nice Work!
You just built a fully functional Pricing Page Architect knowledge base in Neo4j. You can now feed in any set of pricing requirements and get an instant, ranked recommendation for which subscription platform to use.
Here's what you learned:
- Built a multi-platform subscription billing knowledge base in Neo4j with nodes representing Stripe, Chargebee, and Recurly entities, connected by HAS_ENTITY, EQUIVALENT_TO, and REQUIRES relationships.
- Wrote Cypher queries that answer real consulting questions like "which platform needs the fewest API objects for my pricing page?" using weighted relationship traversals and aggregation.
- Experienced the graph database advantage first-hand: cross-platform equivalence lookups and multi-hop traversals that would require recursive CTEs or 10+ JOINs in MySQL resolved in a single Cypher pattern match.
- Secret Mission: Added migration path queries showing entity-by-entity mappings when moving pricing setups between platforms.
Ready to quiz yourself?