Enforce Microservice Data Boundaries
Isolate Order and Inventory databases with Docker Compose networks.
Introduction
30 Second Summary
A checkout can appear reliable while one part quietly reaches into another part's private data. That shortcut can turn a small internal change into a failure somewhere else.
In this project, you will enforce database ownership boundaries in an existing checkout stack with Docker Compose. The finished topology preserves the intended HTTP path while isolating each private database.
What You'll Build
Your final demo shows Order reaching Inventory through its HTTP API while both private databases remain unreachable across service boundaries.
By the end of this project, you'll have:
- A safe boundary demonstration where Order reads Inventory's private schema. The probe produces negative stock without persisting the damage.
- A blocked database shortcut that prevents Order from discovering Inventory's PostgreSQL database. The same boundary keeps Inventory away from Order's data.
- A working API-only checkout where an oversized order returns HTTP 409 from Inventory's business rule. You will also see HTTP 503 when the intended service path is deliberately disconnected.
- Secret Mission: Harden both database networks with internal: true while preserving healthy checkout behavior through the shared API network.
Are there any prerequisites?
You need the persistent-checkout workspace from Persistent Checkout Microservices on a Linux device. Your Product Catalog configuration can remain in compose.override.yaml because this project leaves it untouched.
Before We Start
This checkpoint locks in the boundary you are about to enforce before any configuration changes. order-service may reach order-db plus inventory-service over HTTP, but it must never reach inventory-db directly because direct SQL would bypass Inventory's schema ownership, stock validation, idempotency, authorization, audit behavior, and API contract.
Verify the Existing Linux Stack
A stale workspace can look like a network-isolation failure. An unavailable container runtime can create the same confusion.
This step verifies your local development toolchain. You will also prove that the inherited persistent-checkout stack is healthy before changing its network topology.
In this step, get ready to:
- Verify the three local tools.
- Validate the inherited workspace.
- Confirm the baseline APIs are healthy.
Verify the local tools
The stack depends on Docker Engine to run containers. Docker Compose manages those containers as one application.
You will use Visual Studio Code to inspect the inherited workspace. These checks separate tool problems from network problems later.
- Use your Linux application launcher to search for Terminal.
- Select the terminal application from the search results.
- Check Docker Engine, Docker Compose, and Visual Studio Code by running these commands:
docker version
docker compose version
code --version
What do these checks prove?
- The first command confirms that the Docker client can communicate with the Docker server.
- The second command confirms that Docker Compose is available.
- The third command prints the Visual Studio Code version information.
- Choose the tab that matches the results in your terminal.
✔️ All three checks work
Your local toolchain is ready. Docker reports both Client and Server sections while Compose and Visual Studio Code print version information.
- Continue to the inherited workspace check.
ⓧ Docker or Compose is not ready
Docker Engine must be running before the stack can start. The Docker installation also needs the Compose plugin.
- Follow the official Docker Engine installation guide for your Linux distribution.
- Complete the Docker Engine installation steps for your distribution.
- Return to the terminal after installation.
- Repeat the three checks above.
ⓧ Visual Studio Code is not ready
The Visual Studio Code command must be available from your Linux terminal. Its installation package normally adds the command to your system path.
- Follow the official Visual Studio Code Linux setup guide for your distribution.
- Complete the Visual Studio Code installation steps for your distribution.
- Return to the terminal after installation.
- Repeat the three checks above.
Still missing a tool?
Close your current terminal after installing the missing tool. Start a new terminal session so it can load the updated command path.
If Docker shows client information without a server section, confirm that Docker Engine is running.
Ask for help with the failing check: Help me diagnose my Linux tool check.
Open and validate the inherited workspace
The inherited workspace contains the service definitions you are about to isolate. Validating it first confirms that Compose can understand the current configuration.
- Use your Linux application launcher to search for Visual Studio Code.
- Select Visual Studio Code from the search results.
- Click File in the top menu.
- Click Open Folder....
- Select the existing persistent-checkout folder.
- Click Select Folder.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
- Confirm that compose.yaml, order-service/main.py, and inventory-service/main.py appear in the Explorer view.
The persistent-checkout folder is now your active Visual Studio Code workspace. Its files provide the baseline for every network change in this project.
- Right-click the persistent-checkout folder in the Explorer view.
- Select Open in Integrated Terminal.
- Render the current Compose model by running this command:
docker compose config
What does this command check?
Docker Compose merges the files that define the application. It resolves the configuration into the model that Docker Engine receives.
If compose.override.yaml exists beside compose.yaml, Compose includes it automatically. This preserves any inherited Catalog resources without changing them here.
- Review the rendered output for the Order and Inventory services.
- Confirm that the command finishes without a validation error.
You will see a merged Compose configuration in the terminal. A successful return confirms that the inherited files form a valid application model.
Compose configuration not rendering?
Confirm that the integrated terminal is using the persistent-checkout folder. Compose needs to find compose.yaml there.
If the output identifies an override problem, inspect compose.override.yaml for incomplete YAML from the earlier project.
Get help with the rendered configuration: Help me diagnose my Compose configuration.
Start the baseline stack
A valid configuration proves that Compose can read the files. A running baseline proves that the inherited services still work before network isolation begins.
Before you run the stack, do you expect Compose to report both services as running or healthy?
- Build the images and start the inherited stack by running this command in the integrated terminal:
docker compose up --build --wait
What does this command do?
- The build option creates fresh service images from the inherited source files.
- Compose starts the application containers defined by the merged configuration.
- The wait option keeps the command active until the services are running or healthy.
The command returns successfully when the inherited stack reaches its expected running state. That result gives you a clean baseline for the boundary experiment.
Stack not reaching a healthy state?
Check that Docker Engine still reports a Server section. A client-only result means the container runtime is unavailable.
Inspect the build output for the first service that failed. That service usually points to the stale file or unavailable dependency.
Get help with the startup output: Help me diagnose the baseline stack.
Before you send the health requests, what response should confirm that both inherited APIs are ready?
- Open the Order documentation at http://127.0.0.1:8000/docs in your browser.
- Find the /health operation on the Order documentation page.
- Use the interactive controls to send the Order health request.
- Open the Inventory documentation at http://127.0.0.1:8001/docs in your browser.
- Find the /health operation on the Inventory documentation page.
- Use the interactive controls to send the Inventory health request.
You will see {"status":"ok"} from both health operations. Your inherited Order and Inventory APIs are now confirmed healthy.
Your Linux tools, inherited workspace, and baseline APIs are ready. Next, you will expose why direct database access breaks the Inventory service boundary.
Prove Direct Access Bypasses Inventory Rules
Your baseline checkout stack is healthy. Its implicit Docker Compose network still lets every service discover every other service by name.
That reachability creates schema coupling when Order reads Inventory's private tables. A direct write can also bypass the stock rule owned by Inventory's API.
In this step, get ready to:
- Add a temporary probe that crosses Inventory's database boundary.
- Demonstrate a rolled-back write that bypasses Inventory's stock rule.
- Confirm Inventory's stored quantity remains unchanged.
Add a safe boundary-violation probe
The temporary FastAPI route uses Psycopg to open a connection from Order to Inventory's database. Its transaction reads Inventory's private schema before attempting an invalid stock update.
The file already imports psycopg. It also imports dict_row.
- Switch back to order-service/main.py in Visual Studio Code.
- Scroll to the end of the file.
- Append the temporary route by copying this code at the end of the file:
@app.post("/boundary-probe")
def boundary_probe() -> dict:
with psycopg.connect(
"host=inventory-db dbname=inventory user=inventory_user password=local_inventory_password connect_timeout=10"
) as connection:
with connection.cursor(row_factory=dict_row) as cursor:
before = cursor.execute(
"SELECT sku, quantity FROM inventory WHERE sku = %s",
("keyboard",),
).fetchone()
bypassed = cursor.execute(
"""
UPDATE inventory
SET quantity = quantity - %s
WHERE sku = %s
RETURNING sku, quantity
""",
(999999, "keyboard"),
).fetchone()
connection.rollback()
return {
"read_private_schema": before,
"bypassed_inventory_validation": bypassed,
"rolled_back": True,
}
What does this probe do?
- The connection string targets inventory-db directly from Order.
- The first query depends on the private inventory table plus its sku and quantity columns.
- The update subtracts 999999 from the stored keyboard quantity.
- The connection.rollback() call prevents the invalid quantity from becoming durable.
- The returned fields separate the private read from the bypassed validation result.
- Save order-service/main.py.
Rebuild and run the probe
A container image captures the application code used by a service. Rebuilding Order places the temporary route into the running API.
This rebuild can take a few minutes while Docker refreshes the service image.
- Rebuild the images and start the stack by running:
docker compose up --build --wait
The command returns successfully once the services are running or healthy.
Before you run the probe, predict whether Order can reach Inventory's database directly.
- Return to the Order documentation from earlier at http://127.0.0.1:8000/docs.
- Refresh the documentation page.
- Expand POST /boundary-probe.
- Run the operation using the page's interactive control.
The response shows the current keyboard row under read_private_schema. It shows a negative quantity under bypassed_inventory_validation.
The rolled_back field is true. You have exposed the risky path without persisting the damage.
Probe missing or failing?
If the route is missing from the documentation, confirm that order-service/main.py is saved. Run the rebuild command again after saving.
If the database connection fails, confirm that compose.yaml still uses the inherited default network. Explicit service networks belong to the next step.
Help me troubleshoot the boundary probe.
Confirm the stored quantity stayed safe
A rollback ends the attempted transaction without persisting its update. Reading Inventory through its own endpoint confirms the durable database state.
Before you read Inventory, predict whether the keyboard quantity will keep the negative value.
- Return to the Inventory documentation from earlier at http://127.0.0.1:8001/docs.
- Expand GET /inventory.
- Run the operation using the page's interactive control.
You'll see that the stored keyboard quantity matches the earlier value from read_private_schema. The negative result from the probe was discarded.
Why is direct database access unsafe?
Order now depends on Inventory's private table names. It also depends on Inventory's private column names.
An Inventory schema change can force a matching Order release. That coupling limits independent service changes.
The direct path bypasses authorization at Inventory's API boundary. It also bypasses API-level observability.
A direct SQL write can violate stock rules. It can also skip idempotency controls.
That proof is now concrete: the default network lets Order pierce Inventory's ownership boundary. Next, you will enforce that boundary with explicit networks.
Split the Data Networks
Your temporary probe has proved that Order can reach Inventory's private database through the default network. That proof gives you a clear boundary to enforce.
The default Docker Compose network gives every service a shared communication space. You will replace that broad access with named networks that express which connections each service owns.
Connectivity rules affect every path through the stack. You will segment the database paths first, then test which direct SQL and API connections survive.
In this step, get ready to:
- Declare separate API and data networks.
- Attach each database and API to its checkpoint networks.
- Prove the direct database path and checkout path are blocked.
Declare the three network roles
A top-level network declaration gives each communication path a name. Service-level assignments decide which containers can use that path.
- In compose.yaml, add this block beneath the existing volumes declarations:
networks:
checkout-api:
order-data:
inventory-data:
What do these networks represent?
- The checkout-api network represents communication between the APIs.
- The order-data network represents Order's private database path.
- The inventory-data network represents Inventory's private database path.
- Save compose.yaml.
- Confirm Compose recognizes the declarations by running this command in the terminal from earlier:
docker compose config
What does this command check?
This command merges the Compose files and renders the model that Docker Engine receives. A successful render confirms the new network declarations use valid Compose syntax.
You'll see a rendered configuration containing checkout-api, order-data, and inventory-data.
Configuration not rendering?
Check that networks: begins at the left edge of compose.yaml. Make sure each network name is indented by two spaces.
If compose.override.yaml exists from the earlier project, Compose merges it automatically. Leave that file unchanged.
Help me diagnose my Compose configuration.
The database containers need the narrowest assignments. Each database joins only the data network owned by its service.
- In compose.yaml, add this block beneath the order-db healthcheck:
networks:
- order-data
What does this assignment do?
The service-level list attaches order-db to order-data only. Network membership now reflects Order's ownership of its database.
- Save compose.yaml.
- Render the updated model by running this command:
docker compose config
What should the rendered model show?
The rendered order-db service should list order-data as its only network. This confirms the first database assignment is valid.
- In compose.yaml, add this block beneath the inventory-db healthcheck:
networks:
- inventory-data
How is Inventory isolated?
The assignment places inventory-db on inventory-data only. Containers outside that network cannot use Compose service discovery to reach the database by its service name.
- Save compose.yaml.
- Verify both database assignments by running this command:
docker compose config
What have you established?
The rendered model should show order-db on order-data. It should show inventory-db on inventory-data.
Attach each API deliberately
An API needs access to its own database network. The Order API also joins the network reserved for checkout coordination.
- In compose.yaml, add this block beneath the inventory-service dependency settings:
networks:
- inventory-data
What can Inventory reach?
The Inventory API shares inventory-data with inventory-db. This preserves Inventory's access to the database it owns.
- Save compose.yaml.
- Confirm the Inventory assignment by running this command:
docker compose config
What should Inventory list?
The rendered inventory-service section should list inventory-data as its only network. Its database remains reachable through that private path.
- In compose.yaml, add this block beneath the order-service dependency settings:
networks:
- order-data
- checkout-api
Why does Order join two networks?
The Order API uses order-data to reach its own database. The checkout-api membership reserves a separate path for service coordination.
- Save compose.yaml.
Before you render the final checkpoint, which network do you expect each database and API to list?
- Render the completed checkpoint topology by running this command:
docker compose config
Read the rendered topology
- The order-db service lists only order-data.
- The inventory-db service lists only inventory-data.
- The order-service service lists order-data and checkout-api.
- The inventory-service service lists only inventory-data.
Seeing the wrong network lists?
Make sure each service-level networks block uses the same indentation as environment and depends_on. A block nested inside depends_on belongs to the wrong section.
Help me check my service network assignments.
✔️ Awesome, I've got everything!
Your compose.yaml file now describes the segmented checkpoint. Double check that you saved the file.
ⓧ I'd like to double check the full code
Compare your compose.yaml file with this complete checkpoint configuration.
services:
order-db:
image: postgres:18.6
environment:
POSTGRES_DB: orders
POSTGRES_USER: order_user
POSTGRES_PASSWORD: local_order_password
volumes:
- order-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- order-data
inventory-db:
image: postgres:18.6
environment:
POSTGRES_DB: inventory
POSTGRES_USER: inventory_user
POSTGRES_PASSWORD: local_inventory_password
volumes:
- inventory-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- inventory-data
inventory-service:
build: ./inventory-service
ports:
- "127.0.0.1:8001:8000"
environment:
PGHOST: inventory-db
PGPORT: "5432"
PGDATABASE: inventory
PGUSER: inventory_user
PGPASSWORD: local_inventory_password
depends_on:
inventory-db:
condition: service_healthy
networks:
- inventory-data
order-service:
build: ./order-service
ports:
- "127.0.0.1:8000:8000"
environment:
PGHOST: order-db
PGPORT: "5432"
PGDATABASE: orders
PGUSER: order_user
PGPASSWORD: local_order_password
depends_on:
order-db:
condition: service_healthy
inventory-service:
condition: service_started
networks:
- order-data
- checkout-api
volumes:
order-data:
inventory-data:
networks:
checkout-api:
order-data:
inventory-data:
Experience both effects of the split
The probe and checkout operation exercise different dependencies. Applying the topology lets you test the direct database route first, then the service API route.
- Apply the segmented topology by running this command:
docker compose up --build --wait
What does this command apply?
Compose rebuilds the service images and recreates the affected containers on their assigned networks. The command waits until the services are running or healthy before returning.
Stack not reaching a ready state?
Check the rendered configuration again if a database cannot become healthy. Each API must still share its matching data network with its own database.
Help me troubleshoot the segmented stack.
Before you run the probe, do you think Order can still resolve inventory-db from its assigned networks?
- Return to the Order documentation at http://127.0.0.1:8000/docs.
- Expand the POST /boundary-probe operation.
- Use the operation controls to send the request.
You'll see HTTP 500 within the ten-second connection timeout. This failure is intentional because inventory-db is no longer discoverable from Order.
What did the first failure prove?
The same route that previously read and updated Inventory's private table can no longer establish its database connection. The network now enforces the ownership rule that the code violated.
- Prepare the checkout test by copying this request body:
{"client_reference":"boundary-test-001","sku":"keyboard","quantity":999999}
Why use this request?
- The client_reference value gives the test a stable request identity.
- The quantity value deliberately exceeds the available keyboard stock.
- A reachable Inventory API would evaluate this request against its stock rule.
Before you send the checkout, do you think Order can reach the Inventory API through the current network assignments?
- Expand the POST /orders operation in the Order documentation.
- Replace the example request body with the copied JSON.
- Use the operation controls to send the request.
You'll see HTTP 503 because order-service and inventory-service do not share a network at this checkpoint. The database boundary works, but checkout coordination has lost its intended service path.
Why the two failures matter
The HTTP 500 response proves that Order cannot reach Inventory's database directly. The HTTP 503 response proves that network policy also controls legitimate service communication.
This contrast makes the next requirement concrete. The APIs need a shared path that does not expose either private database.
You have blocked Order's direct route to Inventory's database and exposed the missing API connection. Next, you will restore only the intended checkout path.
Restore the Intended API Path
Your segmented Docker Compose topology now protects both databases. The previous test also exposed a missing connection because checkout returned HTTP 503.
The checkout-api network gives both services a shared API path without exposing either database. Inventory keeps its schema and business rules behind its contract.
In this step, get ready to:
- Connect Inventory to the shared API network.
- Remove the temporary direct database probe from Order.
- Prove that Inventory is reachable and enforcing its stock rule.
Connect Inventory to the API network
Docker service discovery works between services that share a network. Inventory needs both its private data network and the shared API network.
- Return to the persistent-checkout workspace in Visual Studio Code.
- Select compose.yaml from the file sidebar.
- Find this network list inside inventory-service:
networks:
- inventory-data
What does this show?
Inventory currently reaches only inventory-data. Order cannot discover inventory-service from checkout-api in this topology.
- Add checkout-api as the second network entry so the block looks like this:
networks:
- inventory-data
- checkout-api
How does this restore the path?
- The inventory-data entry preserves Inventory's private connection to inventory-db.
- The checkout-api entry lets Order reach Inventory through the service name inventory-service.
- Neither database joins checkout-api.
- Save compose.yaml.
✔️ Awesome, I've got everything!
You've defined the intended API path. Inventory now has its private data connection plus the shared service connection.
ⓧ I'd like to double check the full code
Compare your saved compose.yaml with this complete checkpoint:
services:
order-db:
image: postgres:18.6
environment:
POSTGRES_DB: orders
POSTGRES_USER: order_user
POSTGRES_PASSWORD: local_order_password
volumes:
- order-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- order-data
inventory-db:
image: postgres:18.6
environment:
POSTGRES_DB: inventory
POSTGRES_USER: inventory_user
POSTGRES_PASSWORD: local_inventory_password
volumes:
- inventory-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- inventory-data
inventory-service:
build: ./inventory-service
ports:
- "127.0.0.1:8001:8000"
environment:
PGHOST: inventory-db
PGPORT: "5432"
PGDATABASE: inventory
PGUSER: inventory_user
PGPASSWORD: local_inventory_password
depends_on:
inventory-db:
condition: service_healthy
networks:
- inventory-data
- checkout-api
order-service:
build: ./order-service
ports:
- "127.0.0.1:8000:8000"
environment:
PGHOST: order-db
PGPORT: "5432"
PGDATABASE: orders
PGUSER: order_user
PGPASSWORD: local_order_password
depends_on:
order-db:
condition: service_healthy
inventory-service:
condition: service_started
networks:
- order-data
- checkout-api
volumes:
order-data:
inventory-data:
networks:
checkout-api:
order-data:
inventory-data:
What does the full topology enforce?
Each database appears only on its matching data network. The two API services share only checkout-api.
Remove the direct database probe
The probe has served its diagnostic purpose. Removing its complete function clears Inventory's database hostname, credentials, queries, table names, and column names from Order.
Order now depends on Inventory's HTTP contract. Inventory remains free to change its private schema without forcing Order to change with it.
- Select order-service/main.py from the file sidebar.
- Find the decorator @app.post("/boundary-probe").
- Select the complete /boundary-probe route from its decorator through the closing brace of its returned dictionary.
- Delete the selected route.
- Save order-service/main.py.
- Confirm the whole function is gone by searching order-service/main.py for boundary_probe.
You should see no remaining match for boundary_probe. The direct Inventory database path is gone from Order's source code.
Still finding the probe?
Check that you removed the decorator and the entire boundary_probe() function. A partial deletion can leave database details or invalid Python indentation behind.
Confirm you are searching inside order-service/main.py.
Help me remove the complete boundary probe safely.
Rebuild and prove the boundary
The earlier 503 response captured a missing service path. A 409 response proves that Order reached Inventory before Inventory rejected the invalid quantity.
- Return to the integrated terminal for persistent-checkout.
- Validate the merged network configuration by running:
docker compose config
What does this command confirm?
This command renders the configuration that Docker Compose applies. It catches invalid YAML before the stack is recreated.
You should see inventory-service listing inventory-data and checkout-api. Each database should still list only its matching data network.
Configuration not rendering?
Check the indentation beneath the inventory-service network list. Both entries must line up beneath networks.
Help me fix my Compose network indentation.
- Rebuild the services and apply the new network topology by running:
docker compose up --build --wait
What does this command apply?
Docker Compose rebuilds the service images before starting the stack. The wait flag keeps the command active until the services are running or healthy.
The command should return successfully. Your segmented stack is now running with the restored API path.
Stack not becoming healthy?
Check that inventory-service still lists inventory-data. Its database dependency needs that private network.
Check that both API services list checkout-api.
Help me diagnose the segmented Compose stack.
- Return to the Order documentation at http://127.0.0.1:8000/docs.
- Refresh the documentation page.
- Confirm POST /boundary-probe is absent from the available operations.
- Select POST /orders in the interactive documentation.
- Prepare the over-sized checkout by entering this request body:
{"client_reference":"boundary-test-001","sku":"keyboard","quantity":999999}
What does this request test?
The quantity is far above the available keyboard stock. The request forces Inventory to apply its stock validation through the intended service contract.
The client_reference value keeps this request compatible with the inherited checkout workflow.
Before you submit the request, predict whether Order can now reach Inventory through the API.
- Submit the request through the interactive documentation.
You should see HTTP 409 with Insufficient inventory. Inventory is reachable and enforcing its own stock rule.
Still seeing HTTP 503?
Confirm both order-service and inventory-service list checkout-api in the rendered configuration.
Rebuild the stack after saving compose.yaml so the containers receive the new network attachments.
Help me trace the remaining HTTP 503.
That response proves the boundary works. Order reaches Inventory's contract while Inventory's database stays private.
The intended service path is restored without reopening direct database access. Next, you'll recreate the stack and audit whether this boundary survives routine operations.
Recreate and Audit the Stack
Your checkout now uses an explicit Docker Compose network boundary. Order reaches Inventory through its HTTP contract while both databases stay private.
Routine container recreation must preserve that boundary. It must also preserve the durable data held in named volumes.
In this step, get ready to:
- Recreate the containers and networks without deleting the named volumes.
- Verify that stored Order and Inventory rows survive recreation.
- Audit the rendered network attachments for every service.
Recreate without deleting volumes
Container recreation proves that the network design survives normal operations. The named volumes keep database files separate from the containers that use them.
Removing the containers can look destructive. Your stored rows stay in the named volumes because this command omits --volumes.
- Remove the running service containers and Compose networks from the persistent-checkout terminal by running this command:
docker compose down
What does this command remove?
This command removes the service containers and Compose networks. The named volumes remain because the volume-removal option is absent.
- Review the terminal output for the removed service containers and Compose networks.
Your order-data and inventory-data volumes remain available for the replacement containers.
Stack not stopping cleanly?
Confirm the terminal is still inside persistent-checkout. A different folder may point Compose at the wrong project.
If Docker cannot remove a running resource, check for another Compose command using the same stack.
Help me troubleshoot the Compose shutdown.
- Rebuild the images and recreate the stack by running this command:
docker compose up --build --wait
What does this command restore?
The --build option builds the service images before startup. The --wait option keeps the command active until the services are running or healthy.
Compose creates replacement containers and networks. Each database reconnects to its existing named volume.
- Wait for the command to return successfully.
The replacement stack is now running with the intended API topology.
Stack not becoming ready?
Check the terminal output for the service that did not start. A failed database health check can prevent its dependent API from becoming ready.
Confirm that Docker Engine is running before repeating the startup command.
Help me diagnose the Compose startup.
That recreation test is complete. Your containers are fresh while your stored data remains available.
Confirm durable, independent state
The new database containers mount the same volumes used before recreation. Reading through each owning API now proves that persistence still works.
Before the check, one question matters: did the stored rows survive the restart? Keep your prediction in mind as you call both operations.
- Return to the Order documentation at http://127.0.0.1:8000/docs.
- Call GET /orders in the Order documentation.
- Return to the Inventory documentation at http://127.0.0.1:8001/docs.
- Call GET /inventory in the Inventory documentation.
The Order response contains the rows stored before recreation. The Inventory response contains its previously stored stock rows.
Stored rows missing?
Check whether the earlier shutdown command included the volume-removal option. Removing the named volumes also removes the stored database data.
Confirm that compose.yaml still mounts order-data and inventory-data at the database volume paths.
Help me trace the missing data.
Audit the rendered topology
The rendered Compose model shows the configuration that Docker Engine receives. Auditing it confirms that container recreation preserved every intended network attachment.
Before you render the configuration, which network do you expect to be the only one shared by both APIs? Keep your answer in mind as you inspect the output.
- Render the merged Compose model by running this command:
docker compose config
What does this command reveal?
This command renders the model that Compose applies to Docker Engine. It expands the network lists beneath each service so you can audit the effective topology.
- Confirm that order-service lists order-data plus checkout-api.
- Confirm that inventory-service lists inventory-data plus checkout-api.
- Confirm that order-db lists only order-data.
- Confirm that inventory-db lists only inventory-data.
The APIs share only checkout-api. Each database remains confined to its matching data network.
Network list looks different?
Compare the rendered network lists with compose.yaml. A service-level network entry may be missing or attached to the wrong service.
If compose.override.yaml exists from the earlier project, remember that Compose merges it automatically. Focus this audit on the Order and Inventory services.
Help me audit the network topology.
✔️ Awesome, I've got everything!
Great. Your recreated stack retains its stored data and preserves the intended network boundary.
ⓧ I'd like to double check the full code
Compare your compose.yaml file with this complete version:
services:
order-db:
image: postgres:18.6
environment:
POSTGRES_DB: orders
POSTGRES_USER: order_user
POSTGRES_PASSWORD: local_order_password
volumes:
- order-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- order-data
inventory-db:
image: postgres:18.6
environment:
POSTGRES_DB: inventory
POSTGRES_USER: inventory_user
POSTGRES_PASSWORD: local_inventory_password
volumes:
- inventory-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- inventory-data
inventory-service:
build: ./inventory-service
ports:
- "127.0.0.1:8001:8000"
environment:
PGHOST: inventory-db
PGPORT: "5432"
PGDATABASE: inventory
PGUSER: inventory_user
PGPASSWORD: local_inventory_password
depends_on:
inventory-db:
condition: service_healthy
networks:
- inventory-data
- checkout-api
order-service:
build: ./order-service
ports:
- "127.0.0.1:8000:8000"
environment:
PGHOST: order-db
PGPORT: "5432"
PGDATABASE: orders
PGUSER: order_user
PGPASSWORD: local_order_password
depends_on:
order-db:
condition: service_healthy
inventory-service:
condition: service_started
networks:
- order-data
- checkout-api
volumes:
order-data:
inventory-data:
networks:
checkout-api:
order-data:
inventory-data:
Why does this boundary matter?
The API contract prevents schema coupling between Order and Inventory. Inventory can change its private schema or storage technology without forcing an Order release while the HTTP contract remains compatible.
The separate data networks enforce that ownership after every container recreation. The topology turns an architectural rule into a durable control.
You have proved that the boundary survives routine recreation without sacrificing stored data. The checkout stack now keeps service coordination explicit and database ownership enforceable.
Secret mission
Harden the Database Networks
Your data networks already block cross-service database discovery. Mark both as internal to remove their external route. Then prove checkout still reaches Inventory through checkout-api.
Clean Up Your Resources
Clean Up Your Resources
Your local Docker Compose stack costs nothing to run, although its active containers use memory, CPU, and disk. Choose whether to keep it running, pause it, or delete it.
Resources you used:
- Three local Compose networks: checkout-api, order-data, and inventory-data.
- Four local service containers: order-service, inventory-service, order-db, and inventory-db.
- Two local named volumes containing stored database data: order-data and inventory-data.
Keep everything running
No action needed. Choose this if you want to continue testing the segmented checkout stack.
- The Order documentation remains available at http://127.0.0.1:8000/docs.
- The Inventory documentation remains available at http://127.0.0.1:8001/docs.
- Your stored Order and Inventory rows remain in their matching named volumes.
- Your private database networks retain their internal isolation.
Pause - I'll come back to this later
Pausing frees local memory and CPU while keeping the containers, networks, and stored database data ready for later.
- Stop the services from the persistent-checkout terminal from earlier by running this command:
docker compose stop
What Gets Paused?
This stops the service containers without removing them. The named volumes retain your Order and Inventory data.
- Confirm the terminal shows that each service stopped.
- Restart the existing service containers when you return by running this command:
docker compose start
What Gets Restored?
This starts the existing service containers with their previous configuration. Your stored rows remain available through the retained named volumes.
- Open the Order documentation at http://127.0.0.1:8000/docs.
- Open the Inventory documentation at http://127.0.0.1:8001/docs.
You should see both interactive documentation pages load again.
Delete - I don't want to use this again
Deleting the stack permanently removes its stored Order and Inventory data. Your persistent-checkout project files remain on disk, so you can rebuild the stack later.
- Remove the service containers, Compose networks, and declared named volumes from the persistent-checkout terminal from earlier by running this command:
docker compose down --volumes
What Gets Removed?
This removes the Order and Inventory service containers. It also removes checkout-api, order-data, and inventory-data.
The declared named volumes are deleted with their stored database rows. The files inside persistent-checkout stay untouched.
- Confirm the cleanup command completes successfully in the terminal.
- Refresh the Order documentation at http://127.0.0.1:8000/docs.
- Refresh the Inventory documentation at http://127.0.0.1:8001/docs.
Neither documentation page should load. This confirms that the service containers are no longer running.
Nice Work!
Nice Work!
You did it! Your checkout stack now enforces the boundary between Order and Inventory with Docker Compose networks.
You've learned how to:
- Demonstrate schema coupling by reading Inventory's private table from Order. Show that direct SQL bypasses stock validation without persisting the negative quantity.
- Create database isolation with order-data plus inventory-data. Preserve service-to-service traffic through checkout-api.
- Diagnose failure propagation by distinguishing HTTP 503 from HTTP 409. Use each response to identify an unavailable API path or an Inventory rule rejection.
- Secret Mission: Harden both database networks with internal: true. Confirm both APIs remain healthy. Confirm the oversized checkout still receives Inventory's HTTP 409 response through checkout-api.
Ready to quiz yourself?