demo-fixtures.data.ts
29,093 bytes
| 1 | /** |
|---|---|
| 2 | * The hand-written content for the seeded demo debates. Kept apart from the |
| 3 | * expansion logic in demo-fixtures.ts so the prose is easy to edit. No em-dashes |
| 4 | * or curly quotes here on purpose - plain ASCII throughout. |
| 5 | */ |
| 6 | import type { FixtureSpec } from './demo-fixtures'; |
| 7 | |
| 8 | const monolith: FixtureSpec = { |
| 9 | question: |
| 10 | 'Our 4-engineer startup is building a new B2B SaaS. Should we start with a modular monolith or microservices?', |
| 11 | models: ['openai/gpt-4o', 'anthropic/claude-3.5-sonnet', 'google/gemini-pro-1.5'], |
| 12 | chairmanModel: 'x-ai/grok-2-1212', |
| 13 | convergenceModel: 'google/gemini-2.0-flash-001', |
| 14 | convergenceThreshold: 85, |
| 15 | answers: [ |
| 16 | // GPT-4o - pragmatic monolith |
| 17 | `Start with a modular monolith. With four engineers your scarcest resource is engineering attention, and microservices tax it heavily: network calls, distributed transactions, versioned APIs, and a dozen deploy pipelines before you have product-market fit. Build one deployable application, but enforce clean module boundaries - separate packages for billing, auth, and tenanting, with no cross-module database access. That keeps the door open to extracting a service later, the day a specific module actually needs independent scaling or a different runtime. Ship features now; buy the distributed-systems complexity only when a real bottleneck forces you to.`, |
| 18 | // Claude 3.5 Sonnet - nuanced, boundaries are the deliverable |
| 19 | `A modular monolith is the right default, but the value is in the discipline, not the deployment unit. The failure mode for a small team is not "monolith vs microservices", it is a big ball of mud either way. Define your bounded contexts up front - billing, identity, and the core product domain - give each an explicit interface, and forbid reaching across contexts except through those interfaces. Microservices force those boundaries with the network, at the price of operational overhead you cannot afford yet. So get the boundaries for free in a monolith, through module structure and code review. Revisit only when an organizational reason (a team that owns a context) or a scaling reason (a component with wildly different load) makes a separate deployable worth its cost.`, |
| 20 | // Gemini Pro 1.5 - argues for a couple of early services |
| 21 | `It depends on your deployment and scaling story more than on team size. If parts of your product have very different scaling or compliance profiles - a webhook ingester hammered by traffic, or a data-residency-sensitive component - a couple of well-chosen services from day one can be cheaper than retrofitting them later. I would avoid full microservices, but consider a "macroservice" split: the main app plus one or two services drawn along the sharpest scaling or isolation seams. Independent deploys also shrink blast radius, which matters when four people push to production daily. The key is to keep the count tiny and the boundaries obvious.`, |
| 22 | ], |
| 23 | rounds: [ |
| 24 | { |
| 25 | critiques: [ |
| 26 | { |
| 27 | by: 0, |
| 28 | on: [ |
| 29 | { |
| 30 | p: 1, |
| 31 | score: 8, |
| 32 | strengths: ['Correctly identifies the ball-of-mud as the real risk', 'Frames boundaries as the actual deliverable'], |
| 33 | weaknesses: ['Does not give concrete triggers for when to extract a service'], |
| 34 | note: 'Strong on principle, light on the operational specifics a founder needs.', |
| 35 | }, |
| 36 | { |
| 37 | p: 2, |
| 38 | score: 6, |
| 39 | strengths: ['Fair point that scaling and compliance seams can justify an early split'], |
| 40 | weaknesses: [ |
| 41 | 'Underweights the fixed cost of even one extra service for a team of four', |
| 42 | 'Blast radius is better handled by tests and feature flags than by network boundaries at this stage', |
| 43 | ], |
| 44 | note: 'Reasonable exceptions, but the default it implies is too eager to split.', |
| 45 | }, |
| 46 | ], |
| 47 | }, |
| 48 | { |
| 49 | by: 1, |
| 50 | on: [ |
| 51 | { |
| 52 | p: 0, |
| 53 | score: 8, |
| 54 | strengths: ['Concrete rule: no cross-module database access', 'Names the real cost drivers of microservices'], |
| 55 | weaknesses: ['Slightly understates how easily clean modules rot without enforcement'], |
| 56 | note: 'Actionable and correct. I would pair it with automated boundary checks.', |
| 57 | }, |
| 58 | { |
| 59 | p: 2, |
| 60 | score: 6, |
| 61 | strengths: ['Independent deploys and isolation are genuine benefits'], |
| 62 | weaknesses: [ |
| 63 | 'Treats a couple of services as low-cost, which it is not at this size', |
| 64 | 'Compliance and residency rarely bite a pre-PMF startup', |
| 65 | ], |
| 66 | note: 'The exceptions are valid but rare enough not to be the default.', |
| 67 | }, |
| 68 | ], |
| 69 | }, |
| 70 | { |
| 71 | by: 2, |
| 72 | on: [ |
| 73 | { |
| 74 | p: 0, |
| 75 | score: 7, |
| 76 | strengths: ['Pragmatic and shippable', 'The extract-later path is sound'], |
| 77 | weaknesses: ['Assumes the future extraction will actually happen; teams often never pay down that debt'], |
| 78 | note: 'Good default, but extract later is easier said than done.', |
| 79 | }, |
| 80 | { |
| 81 | p: 1, |
| 82 | score: 7, |
| 83 | strengths: ['Bounded-contexts framing is exactly right'], |
| 84 | weaknesses: ['Does not address cases where scaling profiles genuinely differ from day one'], |
| 85 | note: 'Principled, but a bit absolutist about avoiding all early services.', |
| 86 | }, |
| 87 | ], |
| 88 | }, |
| 89 | ], |
| 90 | revisions: [ |
| 91 | { |
| 92 | p: 0, |
| 93 | changed: true, |
| 94 | content: `Start with a modular monolith with boundaries you actually enforce. One deployable application; each context (billing, identity, the core domain) lives in its own module with a public interface and its own database schema. Make the boundaries real, not aspirational: add an architecture test to CI that fails the build on cross-context imports or shared tables. Extract a context into its own service only when you hit a concrete trigger - it needs to scale independently, it needs a different runtime or language, it falls under a separate compliance boundary, or a dedicated team will own it. Until one of those is true, one deployable wins. This gives you most of what microservices promise without the operational tax a four-person team cannot afford before product-market fit.`, |
| 95 | summary: 'Added enforcement (CI architecture tests, per-context schemas) and explicit extraction triggers, answering the "clean modules rot" and "when to split" critiques.', |
| 96 | bullets: ['Automated architecture tests in CI', 'A schema per bounded context', 'Named, concrete triggers for extraction'], |
| 97 | }, |
| 98 | { |
| 99 | p: 1, |
| 100 | changed: true, |
| 101 | content: `A modular monolith is the right default, and here is the concrete version. One deployable, with bounded contexts as modules - billing, identity, core domain - each exposing an explicit interface and owning its own tables, enforced both in code review and by an automated boundary check. Extract a context into a service when there is an organizational reason (a team will own it) or a technical one (its load or runtime diverges sharply from the rest). Structuring it this way keeps the boundaries honest now, and makes the eventual extraction mechanical rather than a rewrite.`, |
| 102 | summary: 'Made the principle concrete with explicit extraction triggers and named enforcement, addressing the "too abstract" critique. Converged with the enforced-boundaries view.', |
| 103 | bullets: ['Added explicit extraction triggers', 'Named the enforcement mechanisms'], |
| 104 | }, |
| 105 | { |
| 106 | p: 2, |
| 107 | changed: true, |
| 108 | content: `I will narrow my position. For a four-person, pre-PMF team, start with a modular monolith and enforced boundaries; the fixed cost of even one extra service usually is not worth it, and blast radius is better handled with tests and feature flags at this stage. I would keep one exception: if a component has a genuinely different and punishing scaling profile on day one - a high-volume ingester - or a hard data-residency requirement, splitting just that one component out can be cheaper than retrofitting it later. Otherwise: monolith now, extract on a concrete trigger.`, |
| 109 | summary: 'Conceded that a couple of early services is too eager as a default for this team size. Kept a single narrow exception for a day-one extreme-scaling or residency seam.', |
| 110 | bullets: ['Dropped the macroservice-by-default stance', 'Kept one narrow early-split exception'], |
| 111 | }, |
| 112 | ], |
| 113 | convergence: { |
| 114 | score: 74, |
| 115 | disagreements: [ |
| 116 | { |
| 117 | topic: 'Day-one exceptions', |
| 118 | summary: 'Whether any single component justifies its own service before product-market fit.', |
| 119 | positions: [ |
| 120 | { p: 2, stance: 'One narrow exception (extreme scaling or data residency) can justify an early split.' }, |
| 121 | { p: 0, stance: 'Even those are better served by a well-isolated module now, extracted on a real trigger.' }, |
| 122 | ], |
| 123 | }, |
| 124 | ], |
| 125 | }, |
| 126 | }, |
| 127 | { |
| 128 | critiques: [ |
| 129 | { |
| 130 | by: 0, |
| 131 | on: [ |
| 132 | { p: 1, score: 9, strengths: ['Now fully concrete: triggers plus enforcement'], weaknesses: ['Could name the specific architecture-test tooling'], note: 'Essentially aligned. Excellent.' }, |
| 133 | { p: 2, score: 8, strengths: ['Good concession; the narrow exception is defensible'], weaknesses: ['The exception risks becoming a slippery slope without a hard bar'], note: 'Agree, provided the exception stays truly exceptional.' }, |
| 134 | ], |
| 135 | }, |
| 136 | { |
| 137 | by: 1, |
| 138 | on: [ |
| 139 | { p: 0, score: 9, strengths: ['CI boundary checks and per-context schemas make it real'], weaknesses: ['Nothing material'], note: 'The strongest, most actionable version.' }, |
| 140 | { p: 2, score: 8, strengths: ['Reasonable, well-narrowed position'], weaknesses: ['Would require the day-one split to clear a very high bar'], note: 'Close to consensus.' }, |
| 141 | ], |
| 142 | }, |
| 143 | { |
| 144 | by: 2, |
| 145 | on: [ |
| 146 | { p: 0, score: 9, strengths: ['Triggers plus enforcement answer my worry that extraction never happens'], weaknesses: ['Still assumes the team keeps the CI checks green'], note: 'I am largely convinced.' }, |
| 147 | { p: 1, score: 8, strengths: ['Bounded contexts plus triggers'], weaknesses: ['Slightly less concrete than the tooling in the other answer'], note: 'Aligned.' }, |
| 148 | ], |
| 149 | }, |
| 150 | ], |
| 151 | revisions: [ |
| 152 | { |
| 153 | p: 0, |
| 154 | changed: false, |
| 155 | content: `Start with a modular monolith with boundaries you actually enforce. One deployable application; each context (billing, identity, the core domain) lives in its own module with a public interface and its own database schema. Make the boundaries real, not aspirational: add an architecture test to CI that fails the build on cross-context imports or shared tables. Extract a context into its own service only when you hit a concrete trigger - it needs to scale independently, it needs a different runtime or language, it falls under a separate compliance boundary, or a dedicated team will own it. Until one of those is true, one deployable wins. This gives you most of what microservices promise without the operational tax a four-person team cannot afford before product-market fit.`, |
| 156 | summary: 'No substantive change; the position held up under critique. Implementation note: enforce boundaries with a tool like import-linter (Python) or ArchUnit (JVM).', |
| 157 | bullets: [], |
| 158 | }, |
| 159 | { |
| 160 | p: 1, |
| 161 | changed: false, |
| 162 | content: `A modular monolith is the right default, and here is the concrete version. One deployable, with bounded contexts as modules - billing, identity, core domain - each exposing an explicit interface and owning its own tables, enforced both in code review and by an automated boundary check. Extract a context into a service when there is an organizational reason (a team will own it) or a technical one (its load or runtime diverges sharply from the rest). Structuring it this way keeps the boundaries honest now, and makes the eventual extraction mechanical rather than a rewrite.`, |
| 163 | summary: 'Defending the revised position; it converged with the council and needs no further change.', |
| 164 | bullets: [], |
| 165 | }, |
| 166 | { |
| 167 | p: 2, |
| 168 | changed: true, |
| 169 | content: `I will narrow my position. For a four-person, pre-PMF team, start with a modular monolith and enforced boundaries; the fixed cost of even one extra service usually is not worth it, and blast radius is better handled with tests and feature flags at this stage. For the day-one exception, set a hard bar: only pre-split a component if you can point to a specific, quantified scaling or legal requirement that a module provably cannot satisfy - otherwise it stays in the monolith. So: monolith now, one deployable, extract on a concrete trigger.`, |
| 170 | summary: 'Added a hard, quantified bar for the early-split exception so it cannot become a slippery slope, addressing the remaining critique.', |
| 171 | bullets: ['Required a specific, quantified justification for any day-one split'], |
| 172 | }, |
| 173 | ], |
| 174 | convergence: { |
| 175 | score: 91, |
| 176 | disagreements: [], |
| 177 | }, |
| 178 | }, |
| 179 | ], |
| 180 | finalAnswer: `Start with a modular monolith - one deployable application - but treat its internal boundaries as first-class. Split the codebase into bounded contexts (billing, identity, and your core product domain), give each an explicit public interface and its own database schema, and enforce those boundaries automatically with an architecture test in CI that fails on cross-context imports or shared tables. This buys you almost everything microservices promise - clear ownership, replaceable parts, contained blast radius - without the operational tax a four-person team cannot afford before product-market fit: multiple deploy pipelines, network failure modes, distributed transactions, and versioned inter-service APIs. |
| 181 | |
| 182 | Extract a context into its own service only when you hit a concrete, nameable trigger: it needs to scale independently, it requires a different runtime or language, it falls under a separate compliance or data-residency boundary, or a dedicated team will own it. Because the boundaries were enforced from the start, that extraction is mechanical rather than a rewrite. In short: get the discipline of microservices now, effectively for free, through module structure, and pay for their operations only when a real constraint - not a hypothetical one - demands it.`, |
| 183 | dissent: [ |
| 184 | { |
| 185 | topic: 'Splitting a service on day one', |
| 186 | positions: [ |
| 187 | { p: 2, text: 'Would allow one narrow exception: a component with an extreme, quantified scaling or data-residency requirement can justify its own service from the start.' }, |
| 188 | { p: 0, text: 'Even those cases are better served by a well-isolated module initially, extracted the moment the requirement is proven, avoiding any premature distribution.' }, |
| 189 | ], |
| 190 | }, |
| 191 | ], |
| 192 | provenance: [ |
| 193 | { |
| 194 | text: 'Start with one deployable application, split into bounded contexts (billing, identity, core domain) behind explicit interfaces.', |
| 195 | supportedBy: [0, 1], |
| 196 | contestedBy: [2], |
| 197 | }, |
| 198 | { |
| 199 | text: 'Enforce module boundaries automatically with an architecture test in CI that fails on cross-context imports or shared tables.', |
| 200 | supportedBy: [0, 1], |
| 201 | }, |
| 202 | { |
| 203 | text: 'Extract a context into its own service only on a concrete trigger: independent scaling, a different runtime, a compliance boundary, or dedicated ownership.', |
| 204 | supportedBy: [0, 1, 2], |
| 205 | }, |
| 206 | { |
| 207 | text: 'Because the boundaries were enforced from the start, a later extraction is mechanical rather than a rewrite.', |
| 208 | supportedBy: [], |
| 209 | }, |
| 210 | ], |
| 211 | }; |
| 212 | |
| 213 | const restVsGraphql: FixtureSpec = { |
| 214 | question: |
| 215 | 'For a new internal service API used by our web and mobile apps, should we go with REST or GraphQL?', |
| 216 | models: ['openai/gpt-4o', 'anthropic/claude-3.5-sonnet', 'deepseek/deepseek-chat'], |
| 217 | chairmanModel: 'google/gemini-pro-1.5', |
| 218 | convergenceModel: 'google/gemini-2.0-flash-001', |
| 219 | convergenceThreshold: 85, |
| 220 | answers: [ |
| 221 | // GPT-4o - REST default |
| 222 | `Default to REST. For an internal API with two first-party clients, REST over HTTP with clear resource nouns is boring in the best way: cacheable with standard HTTP semantics, trivial to debug with curl, and supported by every proxy and tool you already run. GraphQL earns its keep when you have many heterogeneous clients that each need different slices of data and you want to avoid endpoint sprawl - that is not you yet. Start REST; if the mobile team starts complaining about over-fetching or too many round-trips, add a GraphQL or backend-for-frontend layer for that client specifically.`, |
| 223 | // Claude 3.5 Sonnet - depends on data + clients |
| 224 | `The honest answer is that it depends on your data and your clients, but for two first-party apps I would lean REST with a caveat. If your screens map cleanly to resources, REST is simpler to build, cache, and secure. GraphQL's real win is letting a client select exactly the fields it needs across a graph of related entities in a single request - valuable if your mobile app is on flaky networks and your data is highly relational. Its costs are real too: you own query-complexity limiting, caching gets harder, and authorization moves into resolvers. For most internal APIs, REST plus a few purpose-built aggregate endpoints gets you most of GraphQL's benefit at a fraction of the cost.`, |
| 225 | // DeepSeek - GraphQL for velocity |
| 226 | `I would choose GraphQL. With a web and a mobile client evolving in parallel, a single typed schema becomes a shared contract: clients fetch exactly the fields they need, the backend stops shipping a bespoke endpoint for every screen, and the type system plus introspection give you strong tooling and code generation. Over-fetching and under-fetching both disappear. Yes, you take on caching and query-cost management, but those are well-trodden problems with mature libraries. The developer velocity from not negotiating a new endpoint for every UI change is worth the price.`, |
| 227 | ], |
| 228 | rounds: [ |
| 229 | { |
| 230 | critiques: [ |
| 231 | { |
| 232 | by: 0, |
| 233 | on: [ |
| 234 | { p: 1, score: 8, strengths: ['REST plus aggregate endpoints captures most of the upside cheaply'], weaknesses: ['Does not say when the relational or flaky-network case actually tips to GraphQL'], note: 'Balanced and practical.' }, |
| 235 | { p: 2, score: 6, strengths: ['Right that a shared typed schema speeds client work'], weaknesses: ['Understates caching and authorization cost for a small team', 'Calling those solved problems hand-waves a real operational burden'], note: 'Overweights velocity, underweights ops.' }, |
| 236 | ], |
| 237 | }, |
| 238 | { |
| 239 | by: 1, |
| 240 | on: [ |
| 241 | { p: 0, score: 8, strengths: ['Correct default, with a good tie-break: add a BFF per client if over-fetching bites'], weaknesses: ['Could acknowledge relational data as a genuine GraphQL trigger'], note: 'Solid, if slightly REST-absolutist.' }, |
| 242 | { p: 2, score: 6, strengths: ['The velocity argument is real when UIs change fast'], weaknesses: ['Caching and authorization costs are dismissed too quickly for a small team'], note: 'Good upside case, weak on the downside.' }, |
| 243 | ], |
| 244 | }, |
| 245 | { |
| 246 | by: 2, |
| 247 | on: [ |
| 248 | { p: 0, score: 7, strengths: ['Pragmatic and low-risk'], weaknesses: ['Add GraphQL later underestimates the migration cost once REST endpoints proliferate'], note: 'Safe, but may defer a decision that only gets costlier.' }, |
| 249 | { p: 1, score: 8, strengths: ['Names the exact conditions where GraphQL wins'], weaknesses: ['The aggregate-endpoints path can sprawl too'], note: 'Closest to how I would actually decide.' }, |
| 250 | ], |
| 251 | }, |
| 252 | ], |
| 253 | revisions: [ |
| 254 | { |
| 255 | p: 0, |
| 256 | changed: true, |
| 257 | content: `Default to REST for two first-party clients whose screens map cleanly to resources - it is cheaper to build, cache, secure, and debug. But check two things now, because they are the triggers that tip the balance toward GraphQL: (1) highly relational data where clients routinely need nested, varying slices, and (2) fast-diverging client needs that would otherwise breed a new endpoint per screen. If neither holds, REST plus a handful of aggregate or backend-for-frontend endpoints is the cheaper path. If both hold strongly, start with GraphQL to avoid a costly migration later.`, |
| 258 | summary: 'Added explicit triggers that would justify GraphQL up front, answering the "when does it tip" critique.', |
| 259 | bullets: ['Named the relational-data and diverging-clients triggers'], |
| 260 | }, |
| 261 | { |
| 262 | p: 1, |
| 263 | changed: false, |
| 264 | content: `The honest answer is that it depends on your data and your clients, but for two first-party apps I would lean REST with a caveat. If your screens map cleanly to resources, REST is simpler to build, cache, and secure. GraphQL's real win is letting a client select exactly the fields it needs across a graph of related entities in a single request - valuable if your mobile app is on flaky networks and your data is highly relational. Its costs are real too: you own query-complexity limiting, caching gets harder, and authorization moves into resolvers. For most internal APIs, REST plus a few purpose-built aggregate endpoints gets you most of GraphQL's benefit at a fraction of the cost - just put light governance on those endpoints so they do not sprawl.`, |
| 265 | summary: 'Position held; it already scoped the trade-off. Added that aggregate endpoints need governance to avoid their own sprawl.', |
| 266 | bullets: [], |
| 267 | }, |
| 268 | { |
| 269 | p: 2, |
| 270 | changed: true, |
| 271 | content: `I will qualify my answer. GraphQL is the better fit when data is relational and client needs diverge quickly - common, but not universal. For a small team whose screens map cleanly to resources, REST is genuinely cheaper to operate, and GraphQL's caching and authorization costs are real, not trivial. So: choose GraphQL if you expect many evolving views over connected data; otherwise start REST and add a GraphQL or BFF layer for the client that needs it. The decision should follow the data shape and the rate of UI change, not fashion.`, |
| 272 | summary: 'Conceded that GraphQL\'s operational costs are real, and made the choice conditional on data shape and UI churn rather than a blanket recommendation.', |
| 273 | bullets: ['Made GraphQL conditional on relational data and fast-changing UIs', 'Acknowledged the caching and authorization costs'], |
| 274 | }, |
| 275 | ], |
| 276 | convergence: { |
| 277 | score: 79, |
| 278 | disagreements: [ |
| 279 | { |
| 280 | topic: 'The default choice', |
| 281 | summary: 'Whether to start REST-by-default or GraphQL-by-default for two first-party clients.', |
| 282 | positions: [ |
| 283 | { p: 0, stance: 'REST by default; adopt GraphQL only on clear triggers.' }, |
| 284 | { p: 2, stance: 'GraphQL by default when UIs change fast over connected data.' }, |
| 285 | ], |
| 286 | }, |
| 287 | ], |
| 288 | }, |
| 289 | }, |
| 290 | { |
| 291 | critiques: [ |
| 292 | { |
| 293 | by: 0, |
| 294 | on: [ |
| 295 | { p: 1, score: 9, strengths: ['Clear decision criteria, no dogma'], weaknesses: ['Nothing material'], note: 'The best framing of the three.' }, |
| 296 | { p: 2, score: 8, strengths: ['Good concession; now criteria-driven'], weaknesses: ['Still leans GraphQL in genuinely ambiguous cases'], note: 'Nearly aligned.' }, |
| 297 | ], |
| 298 | }, |
| 299 | { |
| 300 | by: 1, |
| 301 | on: [ |
| 302 | { p: 0, score: 9, strengths: ['The triggers make the default actionable'], weaknesses: ['Nothing material'], note: 'Strong.' }, |
| 303 | { p: 2, score: 8, strengths: ['The conditional recommendation is right'], weaknesses: ['Would set a higher bar before starting with GraphQL'], note: 'Close.' }, |
| 304 | ], |
| 305 | }, |
| 306 | { |
| 307 | by: 2, |
| 308 | on: [ |
| 309 | { p: 0, score: 8, strengths: ['Low-risk and clear'], weaknesses: ['Migration cost if the triggers appear later is still a real risk'], note: 'Fair.' }, |
| 310 | { p: 1, score: 9, strengths: ['Exactly the decision procedure I would use now'], weaknesses: ['Aggregate-endpoint governance is the one open detail'], note: 'Agreed.' }, |
| 311 | ], |
| 312 | }, |
| 313 | ], |
| 314 | revisions: [ |
| 315 | { |
| 316 | p: 0, |
| 317 | changed: false, |
| 318 | content: `Default to REST for two first-party clients whose screens map cleanly to resources - it is cheaper to build, cache, secure, and debug. But check two things now, because they are the triggers that tip the balance toward GraphQL: (1) highly relational data where clients routinely need nested, varying slices, and (2) fast-diverging client needs that would otherwise breed a new endpoint per screen. If neither holds, REST plus a handful of aggregate or backend-for-frontend endpoints is the cheaper path. If both hold strongly, start with GraphQL to avoid a costly migration later.`, |
| 319 | summary: 'No change; the criteria held up under critique.', |
| 320 | bullets: [], |
| 321 | }, |
| 322 | { |
| 323 | p: 1, |
| 324 | changed: false, |
| 325 | content: `The honest answer is that it depends on your data and your clients, but for two first-party apps I would lean REST with a caveat. If your screens map cleanly to resources, REST is simpler to build, cache, and secure. GraphQL's real win is letting a client select exactly the fields it needs across a graph of related entities in a single request - valuable if your mobile app is on flaky networks and your data is highly relational. Its costs are real too: you own query-complexity limiting, caching gets harder, and authorization moves into resolvers. For most internal APIs, REST plus a few purpose-built aggregate endpoints gets you most of GraphQL's benefit at a fraction of the cost - just put light governance on those endpoints so they do not sprawl.`, |
| 326 | summary: 'Defending the position; the council converged on a criteria-based choice.', |
| 327 | bullets: [], |
| 328 | }, |
| 329 | { |
| 330 | p: 2, |
| 331 | changed: false, |
| 332 | content: `I will qualify my answer. GraphQL is the better fit when data is relational and client needs diverge quickly - common, but not universal. For a small team whose screens map cleanly to resources, REST is genuinely cheaper to operate, and GraphQL's caching and authorization costs are real, not trivial. So: choose GraphQL if you expect many evolving views over connected data; otherwise start REST and add a GraphQL or BFF layer for the client that needs it. The decision should follow the data shape and the rate of UI change, not fashion.`, |
| 333 | summary: 'No further change; my qualified, data-shape-driven position matches the emerging consensus.', |
| 334 | bullets: [], |
| 335 | }, |
| 336 | ], |
| 337 | convergence: { |
| 338 | score: 90, |
| 339 | disagreements: [], |
| 340 | }, |
| 341 | }, |
| 342 | ], |
| 343 | finalAnswer: `Default to REST, and reach for GraphQL only on specific triggers. For an internal API serving two first-party clients whose screens map cleanly to resources, REST over HTTP is cheaper to build, cache, secure, and debug - you inherit standard HTTP semantics and every tool in your stack already speaks it. Cover the common over-fetching complaints with a small number of purpose-built aggregate or backend-for-frontend endpoints, and put light governance on them so they do not sprawl. |
| 344 | |
| 345 | Choose GraphQL up front instead when two conditions hold together: your data is highly relational and clients routinely need nested, varying slices of it, and your web and mobile UIs change fast enough that REST would breed a new endpoint per screen. In that world a single typed schema pays for its operational costs - which are real: you must own query-complexity limiting, caching, and resolver-level authorization. Let the data shape and the rate of UI change decide, not fashion. The two clients you have today do not force GraphQL, but a growing, connected data model and fast-diverging clients would.`, |
| 346 | dissent: [ |
| 347 | { |
| 348 | topic: 'Where the default should sit', |
| 349 | positions: [ |
| 350 | { p: 2, text: 'Would set GraphQL as the default whenever UIs are expected to change quickly over connected data, accepting the operational cost for the velocity.' }, |
| 351 | { p: 0, text: 'Keeps REST as the default until the relational-data and diverging-client triggers are clearly met, to avoid paying GraphQL\'s operational tax prematurely.' }, |
| 352 | ], |
| 353 | }, |
| 354 | ], |
| 355 | }; |
| 356 | |
| 357 | export const FIXTURES: FixtureSpec[] = [monolith, restVsGraphql]; |
| 358 | |