{"slug": "confluence-docs-lie-tie-your-documentation-to-code-instead", "title": "\"Confluence Docs Lie. Tie Your Documentation to Code Instead.\"", "summary": "A developer argues that Confluence documentation inevitably goes stale because it's separate from code, and proposes tying documentation directly to code using tools like Spring Boot's springdoc-openapi for API docs and PostgreSQL's COMMENT ON for database schema comments. The approach ensures documentation stays in sync with code changes, reducing documentation debt.", "body_md": "Every team has that Confluence page. The one that was carefully written to explain what the service does, what the API looks like, what each DB column means. Someone spent real time on it. It was accurate when it was written.\n\nSix months later, it's fiction.\n\nThis isn't a discipline problem. I've seen it at teams with strong engineering culture, with good processes, with people who genuinely care about documentation. The Confluence page still goes stale. The root cause is structural, and until you treat it that way, nothing changes.\n\nWhen documentation lives in a different place than the code, there's no mechanism that forces them to stay in sync. It relies entirely on people remembering to update two separate things every time anything changes. A column gets renamed, an endpoint response adds a field, a Kafka message format evolves — the ticket gets closed, the code gets merged, and the Confluence page stays where it was.\n\nAI writing more of your code makes this worse. More code ships faster now. The documentation debt compounds faster too.\n\nThe fix isn't better processes or more reminders in the PR template. It's to stop maintaining documentation as a separate artifact and tie it directly to the code.\n\nIf you're using Spring Boot with springdoc-openapi, you already have the infrastructure. You just need to use it properly.\n\n```\n@RestController\n@RequestMapping(\"/api/orders\")\n@Tag(name = \"Orders\", description = \"Order lifecycle management\")\npublic class OrderController {\n\n    @Operation(\n        summary = \"Get order by ID\",\n        description = \"Returns a single order. 404 if the order doesn't exist or belongs to a different customer.\"\n    )\n    @ApiResponse(responseCode = \"200\", description = \"Order found\")\n    @ApiResponse(responseCode = \"404\", description = \"Order not found\")\n    @GetMapping(\"/{id}\")\n    public ResponseEntity<OrderDto> getOrder(\n        @Parameter(description = \"Internal order ID\") @PathVariable Long id\n    ) {\n        return orderService.findById(id)\n            .map(ResponseEntity::ok)\n            .orElse(ResponseEntity.notFound().build());\n    }\n}\n```\n\nThe Swagger UI becomes a living contract. It's always current because it's generated from the code. Frontend devs, QA, external teams can check it themselves without asking you anything.\n\nIf your controllers are getting buried in annotations, the previous post on [moving Swagger to interfaces](https://dev.to/kirill_f_27d2a0468dd9216e/your-spring-boot-controllers-are-80-swagger-noise-heres-the-fix-1k9h) covers that pattern.\n\nPostgreSQL supports `COMMENT ON`\n\nnatively. It's been there forever, almost nobody uses it.\n\n```\nCREATE TABLE orders (\n    id          BIGSERIAL    PRIMARY KEY,\n    customer_id BIGINT       NOT NULL,\n    status      VARCHAR(20)  NOT NULL,\n    total_cents BIGINT       NOT NULL,\n    created_at  TIMESTAMPTZ  NOT NULL DEFAULT now()\n);\n\nCOMMENT ON TABLE orders IS 'Customer purchase orders. One row per order, regardless of item count.';\n\nCOMMENT ON COLUMN orders.status IS\n    'Order lifecycle state. Valid values: PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED. '\n    'Transitions are enforced in OrderService, not at the DB level.';\n\nCOMMENT ON COLUMN orders.total_cents IS\n    'Order total in cents. Always positive. Divide by 100 for display. '\n    'Never store as decimal — rounding bugs.';\n```\n\nThese comments ship with the schema. They live in your migration files. When the column changes, the comment gets updated in the same commit, by the same person, in the same PR review.\n\nYou can query them directly:\n\n```\nSELECT\n    c.column_name,\n    pgd.description\nFROM pg_catalog.pg_statio_all_tables AS st\nJOIN pg_catalog.pg_description pgd ON pgd.objoid = st.relid\nJOIN information_schema.columns c\n    ON c.table_name = st.relname\n    AND ordinal_position = pgd.objsubid\nWHERE c.table_schema = 'public'\n  AND c.table_name = 'orders';\n```\n\nAnd any decent DB client (DBeaver, DataGrip) surfaces them automatically when you hover over a column. No page to open. The schema explains itself.\n\nIf you're using Avro for Kafka messages, the `doc`\n\nfield is part of the spec. It's not optional in any meaningful sense if you care about your consumers understanding the contract.\n\n```\n{\n  \"type\": \"record\",\n  \"name\": \"OrderCreatedEvent\",\n  \"namespace\": \"com.example.events\",\n  \"doc\": \"Published when a new order is placed. Consumed by inventory-service, billing-service, and notification-service.\",\n  \"fields\": [\n    {\n      \"name\": \"orderId\",\n      \"type\": \"long\",\n      \"doc\": \"Internal order ID from the orders table.\"\n    },\n    {\n      \"name\": \"customerId\",\n      \"type\": \"long\",\n      \"doc\": \"References users.id. The customer who placed the order.\"\n    },\n    {\n      \"name\": \"totalCents\",\n      \"type\": \"long\",\n      \"doc\": \"Order total in cents. Always positive. Same semantics as orders.total_cents.\"\n    },\n    {\n      \"name\": \"status\",\n      \"type\": \"string\",\n      \"doc\": \"Initial status at publish time. Always PENDING for this event.\"\n    }\n  ]\n}\n```\n\nWhen another team's developer needs to understand what this event carries, they read the schema. If the schema is registered in a Schema Registry (Confluent or otherwise), the docs are browsable there. No Confluence page, no hunting down who owns the topic.\n\nThe obvious beneficiaries are engineers. Less time spent answering \"what does this field mean\" questions, better context when onboarding, less cognitive overhead when reading unfamiliar code.\n\nBut the compounding effect shows up with less technical people.\n\nGive your BA a dump of the database DDL — just the schema files, no actual data. Or give them read-only access to a dev environment DB. With an AI assistant they can load those schemas and start asking questions: what do we store, what are the valid states for this field, how are orders and customers related. They get answers without pinging a developer. The schema is always current because it's in version control.\n\nSame with Swagger. A product manager who can open Swagger UI and see the actual endpoints, parameters, and response shapes during a design discussion is a product manager who isn't blocked waiting for you to write up an email.\n\nYour AI coding assistant also benefits. An LLM reading your schema with `doc`\n\nfields and `COMMENT ON COLUMN`\n\nentries has significantly more context than one reading bare column names. The quality of generated code improves with it.\n\nWriting proper Swagger annotations, DDL comments, and Avro doc fields takes time upfront. Real time. When you're under pressure to ship the feature by Friday, adding good `COMMENT ON COLUMN`\n\nentries to the migration doesn't feel like a priority.\n\nIt's also harder to enforce than a Confluence page. With Confluence, you can at least point to a URL and say \"write it here.\" With code-tied docs, you need to make it part of the review culture — PRs that add columns without comments don't get merged.\n\nThat's a real overhead, and I won't pretend otherwise.\n\nBut the alternative is paying that cost continuously. Every time someone joins the team and has to ask what a column means. Every time a BA opens a Jira ticket that could have been answered by looking at the schema. Every time your Confluence page sends someone down the wrong path because nobody updated it after the last refactor.\n\nThe upfront investment is a one-time cost per artifact. Stale docs are a recurring cost forever.\n\nWrite them once. Keep them with the code. They won't lie.\n\nDo you have a practice for keeping docs in sync with code, or is it still Confluence pages and hope? Curious what's actually working at different team sizes.", "url": "https://wpnews.pro/news/confluence-docs-lie-tie-your-documentation-to-code-instead", "canonical_source": "https://dev.to/code_with_kyryl/confluence-docs-lie-tie-your-documentation-to-code-instead-1nnl", "published_at": "2026-06-13 20:23:11+00:00", "updated_at": "2026-06-13 20:44:50.396547+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Confluence", "Spring Boot", "springdoc-openapi", "PostgreSQL", "Swagger UI", "DBeaver", "DataGrip"], "alternates": {"html": "https://wpnews.pro/news/confluence-docs-lie-tie-your-documentation-to-code-instead", "markdown": "https://wpnews.pro/news/confluence-docs-lie-tie-your-documentation-to-code-instead.md", "text": "https://wpnews.pro/news/confluence-docs-lie-tie-your-documentation-to-code-instead.txt", "jsonld": "https://wpnews.pro/news/confluence-docs-lie-tie-your-documentation-to-code-instead.jsonld"}}