Check your own architecture
Describe your services, data stores and calls. Rule checks flag common anti-patterns as you type, each linked to the patterns that fix it. Then ask for an AI-assisted review of the trade-offs.
1. Describe the system
2. Services
3. Data stores
- Used by
- Used by
4. Calls and messages
5. Platform
Import or export as JSON
Diagram
waits for reply message data access. Select a finding to highlight what it is about.
Rule checks
3 high · 10 medium · 2 low- High
Main database is written by 2 services
Orders and Inventory both write to Main database. Nobody owns its schema, so any change needs every team, and one service's locks or load affect the others.
What to do: Give Main database one owning service. Other services use its API, or keep their own copy from its events.
Read: Database per Service, Shared Database, Saga
- High
1 synchronous call has no timeout
Orders → Payments. When the callee slows down, every waiting request holds a thread or connection in the caller until it runs out, and the slowdown spreads upstream.
What to do: Set a connect and request timeout on every call, a little above the callee's normal p99 latency.
Read: Timeout, Circuit Breaker, Bulkhead
- High
Data-changing calls are retried but are not idempotent
Orders → Payments and Orders → Inventory change data and are retried. If the first attempt succeeded but the response was lost, the retry applies the change twice, such as a double charge.
What to do: Send an idempotency key with each request and have the receiver ignore repeats, or stop retrying these calls.
- Medium
Some services wait longer than their callers
Client gives up on Orders after 10000 ms, but Orders waits 15000 ms for Inventory. Inventory gives up on Warehouse after 3000 ms, but Warehouse waits 3000 ms for Shipping. Downstream work continues after nobody is waiting for it.
What to do: Make each inner timeout shorter than the one above it, or pass the remaining deadline down with the request.
Read: Timeout
- Medium
Retries without backoff and jitter
Orders → Payments and Inventory → Warehouse retry immediately. When the callee struggles, every caller retries at once and multiplies its load.
What to do: Use exponential backoff with jitter and a small cap on attempts.
- Medium
One request can become 9 calls at the bottom of a chain
Along Client → Orders → Inventory → Warehouse → Shipping, each layer retries on its own, so a failure at the end is attempted up to 9 times, exactly when that service is least able to cope.
What to do: Retry in one layer only, usually the one closest to the user, and add circuit breakers so retries stop when a dependency is clearly down.
- Medium
A request crosses 4 synchronous hops
Client → Orders → Inventory → Warehouse → Shipping. Latencies add up and availability multiplies down: every service in the chain must be up and fast for the request to succeed.
What to do: Take steps that do not need an immediate answer out of the request path with messaging, or keep a local copy of the data the first service needs.
Read: Asynchronous Messaging, Self-Contained Service, API Composition
- Medium
No circuit breaker in front of services that have their own dependencies
Orders → Inventory and Inventory → Warehouse. When something further down fails, these callers keep sending traffic into it and wait on every request.
What to do: Add a circuit breaker with a fallback, so callers fail fast while the dependency recovers.
Read: Circuit Breaker, Timeout
- Medium
Database writes and published messages can disagree
Orders writes to a database and also publishes messages, without an outbox. If the process fails between the two, the data changes but the message is never sent, or the other way round.
What to do: Write messages to an outbox table in the same transaction as the data, and publish them from there.
- Medium
Orders changes data in 2 services in one request
Orders updates Payments and Inventory synchronously. If a later call fails, the earlier changes stay in place and the services disagree.
What to do: Model the operation as a saga with a compensating action for each step, coordinated by an orchestrator or by events.
Read: Saga, Transactional Outbox
- Medium
Messages are not handled idempotently
Orders → Email. Brokers deliver at least once, so these consumers will sometimes process the same message twice.
What to do: Record each message ID in the same transaction as its effect and skip IDs you have already seen.
- Medium
Clients call 2 services directly
Clients know about Orders and Catalog. Authentication, TLS and rate limiting are repeated in each one, and you cannot reorganise services without breaking clients.
What to do: Put an API gateway in front of the services, and consider a backend for frontend per client type.
- Medium
No distributed tracing
With 7 services, a slow or failed request is hard to follow from one service to the next.
What to do: Propagate trace context on every call and message, and collect spans centrally.
Read: Distributed Tracing
- Low
No dead letter queue for failing messages
Orders → Email. A message that always fails will be retried forever or dropped, and can block the messages behind it.
What to do: Move messages to a dead letter queue after a few failed attempts, alert on it, and make it possible to replay them.
Read: Dead Letter Queue
- Low
No health checks
The platform cannot tell a stuck or still-starting instance from a healthy one, so it keeps sending it traffic.
What to do: Expose separate liveness and readiness endpoints.
Read: Health Check API
AI-assisted review
Sends this design, including the context text, to our review service for a written review of trade-offs. The rule checks above run in your browser. Do not include secrets or personal data.