[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"global":3,"blogpost-extension-functions-with-care":26},{"title":4,"description":5,"home_welcome_badge":6,"home_feature_cards":7,"home_newsletter":20},"Result Crafter Blog","In the era of AI, anyone can generate code overnight. But code that lasts years — you can debug, extend, and maintain — still requires discipline and care.","Welcome to the craft",[8,12,16],{"icon":9,"title":10,"description":11},"code","AI Tools & Workflows","Navigating the new landscape of AI assistance without losing your engineering fundamentals.",{"icon":13,"title":14,"description":15},"bug","Code Smells","Identifying subtle anti-patterns before they become untamable technical debt in your codebase.",{"icon":17,"title":18,"description":19},"cpu","Software Craftsmanship","Techniques for writing clean, testable, and maintainable systems that outlast the current hype cycle.",[21],{"headline":22,"description":23,"placeholder":24,"buttonLabel":25},"Don't miss an essay","Get occasional thoughts on software design, code quality, and building resilient systems delivered straight to your inbox. No spam, ever.","hello@example.com","Subscribe",{"post":27,"directusUrl":226},{"id":28,"title":29,"slug":30,"excerpt":31,"date_created":32,"category":33,"featured_image":34,"content":34,"blocks":35},"12dc0d59-1843-45e9-9024-22825706658e","Extension functions with care","extension-functions-with-care","Extension functions are powerful, but overusing them hurts readability. Here's when to use them and when to avoid.","2026-04-30T10:22:51.485Z","Kotlin",null,[36,44,53,59,65,71,77,83,89,95,101,107,113,119,125,133,140,146,153,158,162,167,175,180,183,188,192,197,202,205,209,213,218,222],{"id":37,"sort":38,"collection":39,"item":40},"e9b1b244-b5a0-430a-9dfd-8183cd089d1f",1,"block_prose",{"id":41,"content":42,"width":43},"289b830f-bb0a-4ec7-bc45-894edf5c9a82","\u003Cp>Extension functions are Kotlin's killer feature. Coming from 10 years of Java, I was so excited about them that I slapped \u003Ccode>fun X.whatever()\u003C\u002Fcode> on \u003Cem>everything\u003C\u002Fem> for the first few months.\u003C\u002Fp>\u003Cp>It felt like magic. String needs a method? Just add it! Date needs formatting? Extend it! Everything became an extension function.\u003C\u002Fp>\u003Cp>Then the bugs started. Name collisions. Hidden side effects. Code that looked simple but was doing database calls. I had to learn the hard way: \u003Cstrong>extension functions are a power tool, not a default\u003C\u002Fstrong>.\u003C\u002Fp>","full",{"id":45,"sort":46,"collection":47,"item":48},"57807d54-f2e7-4153-b75f-b3498f1e4e30",2,"block_code",{"id":49,"code":50,"language":51,"filename":52,"highlight_lines":52,"width":43},17,"\u002F\u002F An extension function looks like a method on String...\nfun String.isEmail(): Boolean {\n    return this.contains(\"@\") && this.contains(\".\")\n}\n\n\u002F\u002F ...but it's actually a static function under the hood\n\u002F\u002F The compiler translates it to:\n\u002F\u002F static boolean isEmail(String $this) {\n\u002F\u002F     return $this.contains(\"@\") && $this.contains(\".\")\n\u002F\u002F }\n\n\"test@example.com\".isEmail() \u002F\u002F true — reads like a method!","kotlin","",{"id":54,"sort":55,"collection":39,"item":56},"0a677959-860d-4fae-b3a8-961912571881",3,{"id":57,"content":58,"width":43},"d3b47bcc-5392-47ef-8989-672a8e2ba405","\u003Cp>This distinction matters more than you think. Because extension functions are \u003Cstrong>static dispatch\u003C\u002Fstrong>, not virtual dispatch. The compiler decides which function to call based on the \u003Cem>declared\u003C\u002Fem> type, not the runtime type. We'll see why this is dangerous later.\u003C\u002Fp>\u003Cp>But first — let's appreciate when extension functions are genuinely great.\u003C\u002Fp>",{"id":60,"sort":61,"collection":39,"item":62},"ec56eb25-296c-48be-8c81-94a03ea584ff",4,{"id":63,"content":64,"width":43},"d57bea79-8d3c-421e-8fb9-14780c4e222a","\u003Ch2>When it's great: readability\u003C\u002Fh2>\u003Cp>The number one reason extension functions exist: \u003Cstrong>code that reads like English\u003C\u002Fstrong>.\u003C\u002Fp>\u003Cp>Compare these two versions of the same logic:\u003C\u002Fp>",{"id":66,"sort":67,"collection":47,"item":68},"daf5b296-4918-4681-8fcf-20ff60d554fa",5,{"id":69,"code":70,"language":51,"filename":52,"highlight_lines":52,"width":43},20,"\u002F\u002F ❌ Without extension — reads like a function call\nval isAdult = calculateAge(user.birthDate) >= 18\nval formatted = formatDate(order.createdAt, \"dd.MM.yyyy\")\nval isValid = validateEmail(user.email)\n\n\u002F\u002F ✅ With extension — reads like a sentence\nval isAdult = user.birthDate.isAdultAge()\nval formatted = order.createdAt.formatRu()\nval isValid = user.email.isValidEmail()",{"id":72,"sort":73,"collection":39,"item":74},"6371a2b0-acde-4d8c-9530-d2a6c2ef07b0",6,{"id":75,"content":76,"width":43},"dd94edd4-1353-4cd6-a60a-b74930d7a8ce","\u003Cp>The second version flows naturally. You don't have to mentally parse function arguments — the subject comes first, just like in English. This is especially valuable in business logic where non-technical stakeholders read the code.\u003C\u002Fp>\u003Cp>Extension functions also shine for utility methods on standard library classes that you can't modify:\u003C\u002Fp>",{"id":78,"sort":79,"collection":47,"item":80},"d3ae9007-2c4e-47ff-98b8-fa5e56945c77",7,{"id":81,"code":82,"language":51,"filename":52,"highlight_lines":52,"width":43},18,"fun LocalDate.formatRu(): String {\n    return this.format(DateTimeFormatter.ofPattern(\"dd.MM.yyyy\"))\n}\n\nval date = LocalDate.of(2026, 4, 8)\ndate.formatRu() \u002F\u002F \"08.04.2026\"\n\n\u002F\u002F Instead of the ugly alternative:\nDateTimeFormatter.ofPattern(\"dd.MM.yyyy\").format(date)",{"id":84,"sort":85,"collection":39,"item":86},"84de0272-71de-43e6-8638-c0591fef1c0a",8,{"id":87,"content":88,"width":43},"13327bd1-2c95-4bcb-a131-db0b9bc65076","\u003Ch2>When it's dangerous\u003C\u002Fh2>\u003Cp>Now the part nobody tells you when you start. Here are the three traps I fell into.\u003C\u002Fp>",{"id":90,"sort":91,"collection":39,"item":92},"f87557f2-aa18-4d54-8fe0-c383015ed3c0",9,{"id":93,"content":94,"width":43},"d5e79d8f-4b5e-4537-93cf-cfd40943f71e","\u003Ch3>\u003Cstrong>Trap 1:\u003C\u002Fstrong> Namespace collisions\u003C\u002Fh3>\n\u003Cp>Two developers, two files, same extension function name on the same type. Who wins? Whoever was imported last. \u003Cem>And the compiler won't warn you.\u003C\u002Fem>\u003C\u002Fp>",{"id":96,"sort":97,"collection":47,"item":98},"72078950-9e34-4de2-9a99-156bd7ff5a18",10,{"id":99,"code":100,"language":51,"filename":52,"highlight_lines":52,"width":43},19,"\u002F\u002F In FileA.kt\nfun String.validate(): Boolean = this.isNotEmpty()\n\n\u002F\u002F In FileB.kt\nfun String.validate(): Boolean = this.matches(Regex(\"[A-Za-z]+\"))\n\n\u002F\u002F Which one gets called? Depends on imports!\n\u002F\u002F This is a naming nightmare in large codebases",{"id":102,"sort":103,"collection":39,"item":104},"8d6ad3cc-d5a5-4853-9afb-621e327526a2",11,{"id":105,"content":106,"width":43},"37ed9013-5daf-4edf-a1e5-e6e1ba6ce832","\u003Ch3>\u003Cstrong>Trap 2:\u003C\u002Fstrong> Hidden complexity\u003C\u002Fh3>\n\u003Cp>An extension function \u003Cem>looks\u003C\u002Fem> like a simple transformation. But nothing stops you from putting side effects inside:\u003C\u002Fp>",{"id":108,"sort":109,"collection":47,"item":110},"d6c9577f-9cf4-45ab-8d74-d3e488c510bc",12,{"id":111,"code":112,"language":51,"filename":52,"highlight_lines":52,"width":43},21,"\u002F\u002F Looks like a simple transformation...\nfun User.toReport(): String {\n    val data = fetchFromDatabase()   \u002F\u002F Side effect!\n    val formatted = ComplexFormatter.format(data)\n    return formatted.encodeToPdf()   \u002F\u002F Also a side effect!\n}\n\n\u002F\u002F When you call it, you expect a simple conversion:\nval report = user.toReport()  \u002F\u002F Did this just hit the DB?!\n\n\u002F\u002F ✅ Better: make it a regular function so the name signals weight\nfun generateUserReport(user: User): String {\n    val data = fetchFromDatabase()\n    val formatted = ComplexFormatter.format(data)\n    return formatted.encodeToPdf()\n}",{"id":114,"sort":115,"collection":39,"item":116},"4ac6dae6-c009-4ec9-ba5a-b2d40105f149",13,{"id":117,"content":118,"width":43},"3d5c6e88-7d1f-4bfe-aadc-b43d5d90617c","\u003Ch3>\u003Cstrong>Trap 3:\u003C\u002Fstrong> Not polymorphic\u003C\u002Fh3>\n\u003Cp>This is the most subtle trap. Extension functions are \u003Cstrong>statically resolved\u003C\u002Fstrong>. The declared type wins, not the runtime type. This can silently call the wrong function:\u003C\u002Fp>",{"id":120,"sort":121,"collection":47,"item":122},"dc493d0e-751c-4928-b735-c4afe18e20c3",14,{"id":123,"code":124,"language":51,"filename":52,"highlight_lines":52,"width":43},22,"open class Animal\nclass Dog : Animal()\n\nfun Animal.speak() = \"Some sound\"\nfun Dog.speak() = \"Woof!\"\n\n\u002F\u002F What does this print?\nval myDog: Animal = Dog()  \u002F\u002F Declared type: Animal\nprintln(myDog.speak())      \u002F\u002F Prints \"Some sound\", NOT \"Woof!\"\n\n\u002F\u002F A regular method would print \"Woof!\" (virtual dispatch)\n\u002F\u002F But extension functions use static dispatch\n\u002F\u002F The declared type (Animal) determines which function is called",{"id":126,"sort":127,"collection":128,"item":129},"6ede211a-e07d-4eb9-a01e-6300ae77fae1",15,"block_diagram",{"id":97,"source_type":130,"mermaid_code":131,"image":34,"caption":132,"width":43},"mermaid","flowchart TD\n    Code[\"val pet: Animal = Dog()\\npet.speak()\"] --> Q{\"Which speak()?\\nDeclared type: Animal\"}\n    Q -->|\"Static dispatch\"| A[\"Animal.speak()\\n→ Some sound\"]\n    Q -.->|\"Virtual dispatch\\n(does NOT happen)\"| D[\"Dog.speak()\\n→ Woof!\"]\n\n    Code2[\"val pet: Dog = Dog()\\npet.speak()\"] --> Q2{\"Which speak()?\\nDeclared type: Dog\"}\n    Q2 -->|\"Static dispatch\"| D2[\"Dog.speak()\\n→ Woof!\"]\n\n    style Code fill:#e3f2fd,stroke:#1565c0,color:#1a1a1a\n    style Q fill:#fff9c4,stroke:#f57f17,color:#1a1a1a\n    style A fill:#ffcdd2,stroke:#c62828,color:#1a1a1a\n    style D fill:#c8e6c9,stroke:#2e7d32,color:#1a1a1a\n    style D2 fill:#c8e6c9,stroke:#2e7d32,color:#1a1a1a\n    style Code2 fill:#e3f2fd,stroke:#1565c0,color:#1a1a1a\n    style Q2 fill:#fff9c4,stroke:#f57f17,color:#1a1a1a","Extension function dispatch: declared type wins, not runtime type",{"id":134,"sort":135,"collection":136,"item":137},"d2343d8c-21c0-4cfb-9fc1-94c609a3eb0c",16,"block_callout",{"id":135,"type":138,"icon":34,"content":139,"width":43},"tip","\u003Cp>\u003Cstrong>My 4 rules for extension functions:\u003C\u002Fstrong>\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cstrong>Pure functions only.\u003C\u002Fstrong> No side effects, no database access, no network calls.\u003C\u002Fli>\u003Cli>\u003Cstrong>Name speaks for itself.\u003C\u002Fstrong> \u003Ccode>String.isEmail()\u003C\u002Fcode> — yes. \u003Ccode>String.check()\u003C\u002Fcode> — no.\u003C\u002Fli>\u003Cli>\u003Cstrong>One abstraction level.\u003C\u002Fstrong> Don't call 5 other extensions inside one.\u003C\u002Fli>\u003Cli>\u003Cstrong>Keep them close to usage.\u003C\u002Fstrong> Not in a shared \u003Ccode>utils.kt\u003C\u002Fcode> with 3000 lines.\u003C\u002Fli>\u003C\u002Ful>",{"id":141,"sort":49,"collection":142,"item":143},"4a752925-000d-4af1-92d5-af4e281bbf05","block_quote",{"id":79,"text":144,"author":145,"source":52,"width":43},"Extension functions are great for readability, utility transformations, and building DSLs. Not for hiding side effects or doing too much.","Summary",{"id":147,"sort":148,"collection":142,"item":149},"cb1aeadb-5c05-4a34-a9a4-805e773bc3f3",999,{"id":55,"text":150,"author":151,"source":152,"width":43},"\u003Cp>API design is a noble and rewarding craft. A well-designed API can be a joy to use and a pleasure to implement. An API should be easy to use correctly and hard to use incorrectly. Extension functions in Kotlin are one tool in this pursuit — use them when they make the API better, not just because you can.\u003C\u002Fp>","Joshua Bloch","Effective Java",{"id":154,"sort":148,"collection":128,"item":155},"a8da5056-e71e-40a7-a486-1ba13f76d112",{"id":46,"source_type":130,"mermaid_code":156,"image":34,"caption":157,"width":43},"flowchart TD\n    A[You want to add a function\nto an existing type] --> B{Do you own\n    the source code?}\n    B -->|Yes| C[Add a member function ✓]\n    B -->|No| D{Is the function used in\n    only one file\u002Fmodule?}\n    D -->|Yes| E[Private extension function ✓]\n    D -->|No| F{Does it read like a\n    natural method on the type?}\n    F -->|Yes| G[Project-level extension\n    with clear docs]\n    F -->|No| H[Use a standalone utility\n    function instead]\n    \n    style C fill:#059669,color:#fff\n    style E fill:#059669,color:#fff\n    style G fill:#d97706,color:#fff\n    style H fill:#059669,color:#fff","Decision flowchart: Should this be an extension function?",{"id":159,"sort":148,"collection":39,"item":160},"5b1a8165-04a2-4287-a0f5-6909e22dfcc2",{"id":161,"content":52,"width":43},"457a6fb5-3acc-40b0-ba24-5f2cff7438f7",{"id":163,"sort":148,"collection":47,"item":164},"1a4c6744-6cae-48c6-b57a-bd16494687a6",{"id":67,"code":52,"language":51,"filename":165,"highlight_lines":166,"width":43},"OverrideSurprise.kt","9,11,12,13,14",{"id":168,"sort":148,"collection":169,"item":170},"59dc1ed6-597d-455d-9d98-ccdb91680533","block_code_text",{"id":67,"code":171,"language":51,"filename":172,"text":173,"layout":174},"\u002F\u002F Real-world utility: truncate with ellipsis\nfun String.truncate(maxLength: Int): String {\n    return if (length \u003C= maxLength) this\n    else substring(0, maxLength - 1) + \"\\u2026\"\n}\n\n\u002F\u002F Real-world utility: pluralize\nfun \u003CT> Collection\u003CT>.pluralize(singular: String, plural: String? = null): String {\n    val p = plural ?: \"${singular}s\"\n    return if (size == 1) \"1 $singular\" else \"$size $p\"\n}\n\n\u002F\u002F Usage is clean and readable\nval headline = \"This is a very long headline that needs trimming\".truncate(30)\nval count = listOf(\"a\", \"b\", \"c\").pluralize(\"item\")\n\u002F\u002F headline = \"This is a very long headline\\u2026\"\n\u002F\u002F count = \"3 items\"","StringUtils.kt","\u003Cp>\u003Cstrong>Good extensions feel like they belong.\u003C\u002Fstrong> A \u003Ccode>truncate()\u003C\u002Fcode> function on \u003Ccode>String\u003C\u002Fcode> reads naturally — it's something you'd expect a String to know how to do. Same with \u003Ccode>pluralize()\u003C\u002Fcode> on a collection.\u003C\u002Fp>\u003Cp>The litmus test: if you showed this to someone unfamiliar with your codebase, would they guess it's an extension or a built-in method? If they'd guess built-in, you've found the sweet spot.\u003C\u002Fp>","code-right",{"id":176,"sort":148,"collection":136,"item":177},"40bd5ebd-e2fa-4f87-a45f-45d914cecf71",{"id":109,"type":178,"icon":34,"content":179,"width":43},"warning","\u003Cp>\u003Cstrong>Member functions always win.\u003C\u002Fstrong> If a class and its extension define the same signature, the member is called — silently. No warning. This is the #1 source of confusion.\u003C\u002Fp>",{"id":181,"sort":148,"collection":128,"item":182},"dc7917c1-6de0-4df0-a0e5-c0b9f3382df8",{"id":46,"source_type":130,"mermaid_code":156,"image":34,"caption":157,"width":43},{"id":184,"sort":148,"collection":39,"item":185},"6b065ebd-9d06-4c5e-ae38-6d51e464002a",{"id":186,"content":187,"width":43},"fb7564f4-f39e-4d73-8711-9316da0d1497","\u003Ch2>When they hurt\u003C\u002Fh2>\u003Cp>Extension functions are resolved \u003Cstrong>at compile time\u003C\u002Fstrong>, not runtime. This means:\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cstrong>You can't override them\u003C\u002Fstrong> — polymorphism doesn't apply\u003C\u002Fli>\u003Cli>\u003Cstrong>They pollute autocomplete\u003C\u002Fstrong> if imported broadly\u003C\u002Fli>\u003Cli>\u003Cstrong>New devs don't know where they live\u003C\u002Fstrong> — is it on the class? An extension? In which file?\u003C\u002Fli>\u003C\u002Ful>\u003Cp>If the function is specific to one file, use a \u003Ccode>private\u003C\u002Fcode> extension. If it's used across the project, consider making it a member function on the class instead.\u003C\u002Fp>",{"id":189,"sort":148,"collection":47,"item":190},"db02155a-186b-4f5c-a8ab-0e3d65f18267",{"id":91,"code":191,"language":51,"filename":34,"highlight_lines":34,"width":43},"\u002F\u002F Truncate a string with ellipsis\nfun String.truncate(maxLength: Int): String {\n    return if (length \u003C= maxLength) this\n    else substring(0, maxLength - 1) + \"\\u2026\"\n}\n\n\u002F\u002F Pluralize a collection\nfun \u003CT> Collection\u003CT>.pluralize(singular: String, plural: String? = null): String {\n    val p = plural ?: \"${singular}s\"\n    return if (size == 1) \"1 $singular\" else \"$size $p\"\n}\n\n\u002F\u002F Usage\nval headline = \"A very long headline\".truncate(15)  \u002F\u002F \"A very long he...\"\nval count = listOf(\"a\", \"b\", \"c\").pluralize(\"item\")   \u002F\u002F \"3 items\"",{"id":193,"sort":148,"collection":39,"item":194},"a6eb8a92-6569-49eb-a060-6f6a6e8c1d9d",{"id":195,"content":196,"width":43},"84363c08-1681-4c0f-b90d-64aa79e5c831","\u003Ch2>When they shine\u003C\u002Fh2>\u003Cp>Extension functions work best in three cases:\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cstrong>Adding utilities to library classes\u003C\u002Fstrong> you don't control — like adding \u003Ccode>isEmail()\u003C\u002Fcode> to \u003Ccode>String\u003C\u002Fcode>\u003C\u002Fli>\u003Cli>\u003Cstrong>Building DSL-style APIs\u003C\u002Fstrong> — Kotlin's collection builders are a great example\u003C\u002Fli>\u003Cli>\u003Cstrong>Making call chains readable\u003C\u002Fstrong> — \u003Ccode>list.filter { it.isValid }.sortedBy { it.priority }\u003C\u002Fcode> reads naturally\u003C\u002Fli>\u003C\u002Ful>\u003Cp>The litmus test: would someone unfamiliar with your codebase guess it's an extension or a built-in? If they'd guess built-in, you've found the sweet spot.\u003C\u002Fp>",{"id":198,"sort":148,"collection":39,"item":199},"997ba466-1a90-497c-a71b-e2b15bd35708",{"id":200,"content":201,"width":43},"f8f74d9c-c443-4cbc-8878-f964b1e96d57","\u003Cp>Extension functions let you add methods to existing classes — without inheritance, without wrappers, without touching the source. Sounds like a superpower. But overuse creates confusion. Let's look at when they help and when they hurt.\u003C\u002Fp>",{"id":203,"sort":148,"collection":136,"item":204},"5d413801-aac8-481b-9770-64a61179176e",{"id":73,"type":178,"icon":34,"content":52,"width":43},{"id":206,"sort":148,"collection":39,"item":207},"499273c4-3506-4c1a-afc9-9a85c2bd38ae",{"id":208,"content":52,"width":43},"ea1b190b-576f-4efe-be24-f20e2b88d4da",{"id":210,"sort":148,"collection":39,"item":211},"1f03702e-8b51-4e3b-9630-1c1c3a1c331e",{"id":212,"content":52,"width":43},"098d6a26-0233-4fc4-b027-58d0b10654b0",{"id":214,"sort":148,"collection":47,"item":215},"12f5ca42-a0e0-4a95-a8a5-d0d68ea58ee4",{"id":61,"code":52,"language":51,"filename":216,"highlight_lines":217,"width":43},"Extensions.kt","2,5,8,9,14,15",{"id":219,"sort":148,"collection":39,"item":220},"d7ce3527-95f3-467a-90d9-9ead4e986be3",{"id":221,"content":52,"width":43},"944b42df-7ff7-420f-979c-7be1bb7c3cda",{"id":223,"sort":148,"collection":39,"item":224},"d755131b-57dc-47bf-93e0-5ec4af6f42fb",{"id":225,"content":52,"width":43},"67402f8a-4558-4acb-904b-f6e0e84a985d","https:\u002F\u002Fd1.resultcrafter.com"]