[{"data":1,"prerenderedAt":671},["ShallowReactive",2],{"blog:en:object-oriented-without-classes":3},{"id":4,"title":5,"author":6,"body":7,"date":657,"description":658,"extension":659,"meta":660,"navigation":356,"path":661,"publishAt":662,"seo":663,"stem":664,"tags":665,"__hash__":670},"blog_en/blog/object-oriented-without-classes/en.md","The Question a Java Developer Asked Us","Aptli",{"type":8,"value":9,"toc":646},"minimark",[10,15,19,22,25,28,32,35,38,41,44,48,51,54,57,60,63,67,70,73,76,172,175,178,181,184,187,190,193,197,200,203,499,518,521,525,528,531,566,569,572,576,579,582,585,588,592,595,598,601,604,607,611,642],[11,12,14],"h2",{"id":13},"the-question","The Question",[16,17,18],"p",{},"A developer applying to work with us came from Java. We were talking about\ncoding style, and they asked which object-oriented design patterns we use.",[16,20,21],{},"We use most of them constantly, we told them. Strategy for a family of\ninterchangeable behaviors. Observer for a change that needs to reach everything\ninterested in it. Composition over inheritance, as a habit more than a rule.",[16,23,24],{},"Their question after that was sharper. How, when our front end declares almost\nno classes and no interfaces? Java expresses those patterns through a base type\nand the things that implement it. Our code does not have that vocabulary\navailable, so it has to be doing the same job through something else.",[16,26,27],{},"It is. A Vue component already holds what an object holds: its own data, its\nown methods, and the part a Java object usually hands off elsewhere, how it\npresents itself. A composable is how one component obtains a behavior from\nanother, the same has-a relationship a class gets from composition, minus the\nbase class. Different vocabulary, same argument, and it held up better than we\nexpected once we sat down to write out exactly how.",[11,29,31],{"id":30},"the-business-problem-underneath-it","The Business Problem Underneath It",[16,33,34],{},"We sell software that our customers configure themselves. A public works\ndepartment builds its own service request forms. A utility defines its own asset\ncategories. A municipality writes its own automation rules for what happens when\na report comes in.",[16,36,37],{},"That means requests arrive constantly, and they are small. Can we add a\nsignature field to this form. Can we pull data from this other system. Can we\nsend that alert by text message as well as email.",[16,39,40],{},"Every one of those is easy to say yes to once. The interesting question is what\nthe tenth one costs. If each new field type means editing the same three files,\nthe tenth request is slower than the first, and the fiftieth is a project. If\neach one is an addition, the fiftieth costs about what the first did.",[16,42,43],{},"Nothing about that is visible in a demo. It is decided years earlier, by how the\ncode is shaped. So we spend real attention on the shape.",[11,45,47],{"id":46},"naming-the-shape","Naming the Shape",[16,49,50],{},"Most advice about code quality lands on two ideas. Do not repeat yourself. Prefer\ncomposition to inheritance. Both are true, and neither settles much at four\no'clock on a Thursday when someone is deciding where a new feature goes.",[16,52,53],{},"What settles it is a shared name.",[16,55,56],{},"When one of us says \"make that a registry,\" everyone on the team knows what\narrives in the review: a table with one row per type, one function per row, and a\nsingle line that looks up the right row. Nobody sketches it on a whiteboard.\nNobody argues about it. The review takes a minute, because the shape was agreed\nbefore anyone wrote code.",[16,58,59],{},"That vocabulary is the thing we actually got from studying patterns. It lives\nbetween people, and it earns its keep twice. It makes\na good decision fast, and it makes a poor one visible while it is still a\nsentence, before it becomes three files.",[16,61,62],{},"We keep a written list of the shapes we use and where each belongs. New work\nstarts by naming which one it is. That single question resolves most of the\ndesign, and the rest is typing.",[11,64,66],{"id":65},"what-that-buys-concretely","What That Buys, Concretely",[16,68,69],{},"Take notifications. When something happens that a person needs to know about, we\nsend it. Email, text message, a push to their phone.",[16,71,72],{},"A customer asked us to add text messages for one particular alert. That is a\nsmall request, and it is exactly the kind we get every week.",[16,74,75],{},"We keep one table of what we send, with one entry per notification. Each entry\nanswers a single question in plain code: who should hear about this?",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","'report.submitted': {\n  recipients: (args) => [args.report.assignee, ...args.report.watchers],\n},\n","ts","",[84,85,86,108,166],"code",{"__ignoreMap":82},[87,88,91,95,99,101,105],"span",{"class":89,"line":90},"line",1,[87,92,94],{"class":93},"sMK4o","'",[87,96,98],{"class":97},"sfazB","report.submitted",[87,100,94],{"class":93},[87,102,104],{"class":103},"sTEyZ",": ",[87,106,107],{"class":93},"{\n",[87,109,111,115,118,121,125,128,132,135,138,141,143,146,149,152,154,156,158,160,163],{"class":89,"line":110},2,[87,112,114],{"class":113},"s2Zo4","  recipients",[87,116,117],{"class":93},":",[87,119,120],{"class":93}," (",[87,122,124],{"class":123},"sHdIc","args",[87,126,127],{"class":93},")",[87,129,131],{"class":130},"spNyl"," =>",[87,133,134],{"class":103}," [args",[87,136,137],{"class":93},".",[87,139,140],{"class":103},"report",[87,142,137],{"class":93},[87,144,145],{"class":103},"assignee",[87,147,148],{"class":93},",",[87,150,151],{"class":93}," ...",[87,153,124],{"class":103},[87,155,137],{"class":93},[87,157,140],{"class":103},[87,159,137],{"class":93},[87,161,162],{"class":103},"watchers]",[87,164,165],{"class":93},",\n",[87,167,169],{"class":89,"line":168},3,[87,170,171],{"class":93},"},\n",[16,173,174],{},"Adding a notification is one entry. Choosing the channels is a matter of which\nmessage templates exist for it, so a text message arrives by writing the text\nversion of the message. The engine that does the sending stays as it is. It is\nabout forty lines long, and it has stayed about forty lines long through every\nnotification we have added.",[16,176,177],{},"It did not start out looking like that. The first notification we ever built was\na toast, the small message that slides into the corner of the screen to say the\nthing you just did worked. A toast is about as simple as this gets, and there was\na version of the job where we wrote it as a toast and moved on.",[16,179,180],{},"We wrote the table instead, because a toast is already answering the two\nquestions everything else would answer. Something happened. Who needs to know,\nand what should they see?",[16,182,183],{},"The answers grew a great deal after that. Some notifications had to reach a\nperson who had closed the tab, so they became email. Some had to reach a crew\nwith no laptop, so they became text messages. Some needed to arrive on a phone at\ntwo in the morning, so they became push. Each of those brought real work with it,\nincluding a new interface for people to choose what they want to hear about, and\nnone of it changed the answer to the original two questions.",[16,185,186],{},"The rule we hold to is that anything unusual about one notification lives inside\nthat notification's own entry. The moment a special case moves into the engine,\nthe engine starts growing a flag per case, and the next person has to read all of\nthem to add anything.",[16,188,189],{},"There is a version of this where the sending code asks \"is this an email, or a\ntext, or a push\" and grows a branch each time. We have both shapes in our\ncodebase, because software written over years is honest about its own history.\nThe branch version is the one we schedule time to convert, and the reason is not\ntidiness. A row is a Tuesday. A branch is a conversation.",[16,191,192],{},"The Java developer would call the first version Strategy. There is no interface\nand no class anywhere in it, and it does exactly what the pattern promises.",[11,194,196],{"id":195},"the-object-was-already-there","The Object Was Already There",[16,198,199],{},"Start with the half of the answer that surprised us least once we looked at it\ndirectly. Java expresses an object as a class: a bundle of data, the methods\nthat act on it, and nothing about how it appears on screen, since that is a\nseparate concern handled elsewhere.",[16,201,202],{},"A Vue component bundles the same three things, plus the third piece Java\nusually hands off:",[77,204,208],{"className":205,"code":206,"language":207,"meta":82,"style":82},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{ userId: string }>()\nconst user = ref\u003CUser | null>(null)\nconst isAdmin = computed(() => user.value?.role === 'admin')\n\nasync function load() { user.value = await getFetch(`/api/users/${props.userId}`) }\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>{{ user?.name }} \u003Cspan v-if=\"isAdmin\">Admin\u003C/span>\u003C/div>\n\u003C/template>\n","vue",[84,209,210,238,269,304,351,358,419,429,434,444,490],{"__ignoreMap":82},[87,211,212,215,219,222,225,228,231,233,235],{"class":89,"line":90},[87,213,214],{"class":93},"\u003C",[87,216,218],{"class":217},"swJcz","script",[87,220,221],{"class":130}," setup",[87,223,224],{"class":130}," lang",[87,226,227],{"class":93},"=",[87,229,230],{"class":93},"\"",[87,232,81],{"class":97},[87,234,230],{"class":93},[87,236,237],{"class":93},">\n",[87,239,240,243,246,248,251,254,257,259,263,266],{"class":89,"line":110},[87,241,242],{"class":130},"const",[87,244,245],{"class":103}," props ",[87,247,227],{"class":93},[87,249,250],{"class":113}," defineProps",[87,252,253],{"class":93},"\u003C{",[87,255,256],{"class":217}," userId",[87,258,117],{"class":93},[87,260,262],{"class":261},"sBMFI"," string",[87,264,265],{"class":93}," }>",[87,267,268],{"class":103},"()\n",[87,270,271,273,276,278,281,283,286,289,292,295,298,301],{"class":89,"line":168},[87,272,242],{"class":130},[87,274,275],{"class":103}," user ",[87,277,227],{"class":93},[87,279,280],{"class":113}," ref",[87,282,214],{"class":93},[87,284,285],{"class":261},"User",[87,287,288],{"class":93}," |",[87,290,291],{"class":261}," null",[87,293,294],{"class":93},">",[87,296,297],{"class":103},"(",[87,299,300],{"class":93},"null",[87,302,303],{"class":103},")\n",[87,305,307,309,312,314,317,319,322,324,327,329,332,335,338,341,344,347,349],{"class":89,"line":306},4,[87,308,242],{"class":130},[87,310,311],{"class":103}," isAdmin ",[87,313,227],{"class":93},[87,315,316],{"class":113}," computed",[87,318,297],{"class":103},[87,320,321],{"class":93},"()",[87,323,131],{"class":130},[87,325,326],{"class":103}," user",[87,328,137],{"class":93},[87,330,331],{"class":103},"value",[87,333,334],{"class":93},"?.",[87,336,337],{"class":103},"role ",[87,339,340],{"class":93},"===",[87,342,343],{"class":93}," '",[87,345,346],{"class":97},"admin",[87,348,94],{"class":93},[87,350,303],{"class":103},[87,352,354],{"class":89,"line":353},5,[87,355,357],{"emptyLinePlaceholder":356},true,"\n",[87,359,361,364,367,370,372,375,377,379,381,384,388,391,393,396,399,402,405,407,410,413,416],{"class":89,"line":360},6,[87,362,363],{"class":130},"async",[87,365,366],{"class":130}," function",[87,368,369],{"class":113}," load",[87,371,321],{"class":93},[87,373,374],{"class":93}," {",[87,376,326],{"class":103},[87,378,137],{"class":93},[87,380,331],{"class":103},[87,382,383],{"class":93}," =",[87,385,387],{"class":386},"s7zQu"," await",[87,389,390],{"class":113}," getFetch",[87,392,297],{"class":217},[87,394,395],{"class":93},"`",[87,397,398],{"class":97},"/api/users/",[87,400,401],{"class":93},"${",[87,403,404],{"class":103},"props",[87,406,137],{"class":93},[87,408,409],{"class":103},"userId",[87,411,412],{"class":93},"}`",[87,414,415],{"class":217},") ",[87,417,418],{"class":93},"}\n",[87,420,422,425,427],{"class":89,"line":421},7,[87,423,424],{"class":93},"\u003C/",[87,426,218],{"class":217},[87,428,237],{"class":93},[87,430,432],{"class":89,"line":431},8,[87,433,357],{"emptyLinePlaceholder":356},[87,435,437,439,442],{"class":89,"line":436},9,[87,438,214],{"class":93},[87,440,441],{"class":217},"template",[87,443,237],{"class":93},[87,445,447,450,453,455,458,460,462,465,467,469,472,474,476,479,481,483,486,488],{"class":89,"line":446},10,[87,448,449],{"class":93},"  \u003C",[87,451,452],{"class":217},"div",[87,454,294],{"class":93},[87,456,457],{"class":103},"{{ user?.name }} ",[87,459,214],{"class":93},[87,461,87],{"class":217},[87,463,464],{"class":130}," v-if",[87,466,227],{"class":93},[87,468,230],{"class":93},[87,470,471],{"class":97},"isAdmin",[87,473,230],{"class":93},[87,475,294],{"class":93},[87,477,478],{"class":103},"Admin",[87,480,424],{"class":93},[87,482,87],{"class":217},[87,484,485],{"class":93},">\u003C/",[87,487,452],{"class":217},[87,489,237],{"class":93},[87,491,493,495,497],{"class":89,"line":492},11,[87,494,424],{"class":93},[87,496,441],{"class":217},[87,498,237],{"class":93},[16,500,501,503,504,507,508,510,511,514,515,517],{},[84,502,404],{}," is a constructor argument. ",[84,505,506],{},"user"," and ",[84,509,471],{}," are instance variables.\n",[84,512,513],{},"load"," is a method. The template is the part Java usually leaves to something\nelse: the piece that decides how the object presents itself. Put two of these\non the same screen for two different users and each holds its own ",[84,516,506],{},",\nprivate to itself, never seeing the other. That is two objects.",[16,519,520],{},"None of that needed the word class. In the patterns we lean on, the object was\nnever really the part in question. What varies is how objects relate to one\nanother: whether one inherits from another, whether a family of behaviors can\nbe swapped out, whether a change in one should reach the others. For the kind\nof application we build, on the framework we build it in, that relating turned\nout to work better as composables and tables than as classes. That is a\nstatement about our own practical fit, not a claim that classes stop being\nuseful elsewhere. Ours still exist, in the one place the language requires\nthem, and the next section is that.",[11,522,524],{"id":523},"where-inheritance-went","Where Inheritance Went",[16,526,527],{},"We went looking, out of curiosity, for every place our code uses inheritance.\nThere are a handful, and all of them are error types, where the language requires\nit.",[16,529,530],{},"What we use instead is composition, which in our world looks like small functions\nthat hand back the pieces you asked for.",[77,532,534],{"className":79,"code":533,"language":81,"meta":82,"style":82},"const { record, canEdit, save } = useRecordEditor(recordId)\n",[84,535,536],{"__ignoreMap":82},[87,537,538,540,542,545,547,550,552,555,558,560,563],{"class":89,"line":90},[87,539,242],{"class":130},[87,541,374],{"class":93},[87,543,544],{"class":103}," record",[87,546,148],{"class":93},[87,548,549],{"class":103}," canEdit",[87,551,148],{"class":93},[87,553,554],{"class":103}," save ",[87,556,557],{"class":93},"}",[87,559,383],{"class":93},[87,561,562],{"class":113}," useRecordEditor",[87,564,565],{"class":103},"(recordId)\n",[16,567,568],{},"That line produces its own working object with its own state. Two editors open at\nonce hold two records and never see each other. A Java developer reads that and\nrecognises an object with public methods, with the rest kept private.",[16,570,571],{},"The book that taught most of us these patterns states this as a principle:\nfavour composition over inheritance. Inheritance ties you to a parent and hands\nyou everything it has. Composition takes the three things you need. Vue's own\ncomposable style already matches that principle closely, so our code settled\nhere without anyone holding a meeting about it. A team building the same\nproduct in Java would still be right to reach for inheritance in places we\nnever do, since the fit depends on the framework as much as the pattern.",[11,573,575],{"id":574},"when-your-customers-are-the-ones-extending-it","When Your Customers Are the Ones Extending It",[16,577,578],{},"The pattern we lean on hardest is the one where a change announces itself and\nanything interested reacts.",[16,580,581],{},"Our automation rules work that way, and the interesting part is who writes them.\nA customer opens the interface and builds a rule: when a report comes in for this\nkind of asset, notify this crew and open a work order. That rule is a row in\ntheir database. When the event fires, the engine finds every rule watching for\nit and runs each one, keeping failures separate so a broken rule leaves the rest\nworking.",[16,583,584],{},"Textbook Observer has programmers registering the listeners. Here the customer\ndoes, through a form, on a Tuesday afternoon, with no release involved.",[16,586,587],{},"That shift brings problems the textbook never had to solve. A rule can fire an\nevent that triggers another rule, so every run carries a budget for how far a\nchain may travel. Rules carry their own rate limits, so one busy sensor leaves\nthe queue available to everyone else. Those two safeguards exist because\ncustomers, quite reasonably, build things we did not anticipate.",[11,589,591],{"id":590},"why-we-talk-about-this","Why We Talk About This",[16,593,594],{},"The question we started with was about patterns, and the real answer is about\nhow we work.",[16,596,597],{},"We use these ideas constantly and you will not find them by searching for the\nkeywords, because our language spells them differently. More usefully, we have a\nshared vocabulary for them, so a design decision is usually a short conversation.",[16,599,600],{},"The toast is the clearest example. It was written in a language and a framework\nthat barely resemble what runs that code today, by people who had not read the\nsame books. The shape underneath it held anyway, and it is still holding now that\nthe same notification can arrive as an email, a text message or a push at two in\nthe morning.",[16,602,603],{},"That is the part worth carrying. The patterns outlast the language used to\ndescribe them. Java calls one thing an interface with implementing classes, we\ncall it a table of functions, and the argument both are making is identical.\nLearn the argument and the syntax stops mattering.",[16,605,606],{},"If you are weighing up whether to work with us, on either side of the table, that\nis what we would want you to know. We think about the shape of a thing before we\nbuild it, we write down what we learn, and we go back and fix the places where we\ngot it wrong. What our customers see is that small requests stay small.",[11,608,610],{"id":609},"summary","Summary",[612,613,614,618,621,624,627,630,633,636,639],"ul",{},[615,616,617],"li",{},"Our customers configure the product themselves, so small requests arrive\nconstantly. The cost of the fiftieth one is decided by how the code is shaped,\nyears before it is asked for.",[615,619,620],{},"The lasting value of design patterns is a shared vocabulary. Agreeing on the\nshape before the code exists turns a design debate into a short review.",[615,622,623],{},"A Vue component already bundles what an object bundles: its own data, its own\nmethods, and how it presents itself. That did not need a class keyword, and\nit is not a claim that classes stop mattering. We still reach for one exactly\nwhere the language requires it, and a team building the same product in Java\nwould be right to reach for inheritance in places we do not.",[615,625,626],{},"We keep behaviour in tables, so a new capability is a row and the engine that\nreads the table stays as written. Our notification engine has held at about\nforty lines through every notification we have added.",[615,628,629],{},"That engine began as a toast in the corner of a screen. Email, text message and\npush each brought real work, including a new interface for choosing what you\nhear about, and none of it changed the shape underneath.",[615,631,632],{},"Inheritance is effectively absent from our front end. Small composable\nfunctions hand back the pieces a screen needs, each with its own state.",[615,634,635],{},"Our automation rules are the customer's own listeners, stored as data. That\nneeds safeguards a textbook version does not: a limit on how far a chain of\nrules can travel, and a rate limit per rule.",[615,637,638],{},"We keep a written list of the shapes we use, and revisit the places that\npredate it.",[615,640,641],{},"Patterns outlast the language used to describe them. The syntax differs between\nJava and JavaScript. The argument each pattern is making does not.",[643,644,645],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":82,"searchDepth":110,"depth":110,"links":647},[648,649,650,651,652,653,654,655,656],{"id":13,"depth":110,"text":14},{"id":30,"depth":110,"text":31},{"id":46,"depth":110,"text":47},{"id":65,"depth":110,"text":66},{"id":195,"depth":110,"text":196},{"id":523,"depth":110,"text":524},{"id":574,"depth":110,"text":575},{"id":590,"depth":110,"text":591},{"id":609,"depth":110,"text":610},"2026-08-30","Which design patterns do you use? Our front end has almost no classes in it, so the honest answer took a while. It turned into a good description of how we decide what to build and how we talk to each other about it.","md",{},"/blog/object-oriented-without-classes/en","2026-09-01",{"title":5,"description":658},"blog/object-oriented-without-classes/en",[666,667,668,669],"engineering","how-we-work","design-patterns","product","3RP-dvguSzrqXEMeUCa4ZZ0VWRsJhlbFhVG037aWQvk",1788431621086]