HACKER Q&A
📣 happycoder97

What are some architectural decisions that improved your codebase?


Dear senior developers on HN, What are some examples of design choices that helped you reduce the effort needed to change your code according to change in requirements? What are some of the architectural choices you made that made your codebase easier to work with?


  👤 ncmncm Accepted Answer ✓
Eliminate threads, queues, locks, buffer allocate & free, copying, system calls, synchronous logging, file ops, dynamic memory allocation.

Replace with huge-page mapped ring buffers, independent processes, kernel-bypass set-and-forget, buffer lap checks, file-mapped self-describing binary-formatted stats, direct-mode disk block writes, caller-provided memory.


👤 benologist
I have been making my test suite emit structured data for the API tests which is used to document the API. This eliminated the margin for error in manually keeping the API documentation up to date. This improved the test coverage a lot as complete coverage is required for the documentation to be complete. It looks great too -

https://github.com/userdashboard/organizations/blob/master/a... derived from https://github.com/userdashboard/organizations/blob/master/t...

Another thing that helped was moving all my UI page tests to Puppeteer which is a NodeJS API for browsing with Chrome and tentatively Firefox web browsers. This let me automatically generate screenshots for my entire UI to publish as documentation, while simultaneously testing the responsive design under different devices which surfaced many issues.

https://userdashboard.github.io/administrators/stripe-subscr... generated by https://github.com/userdashboard/userdashboard.github.io/blo...


👤 gitgud
Stateless components, or as I like to call them dumb components.

We found it much easier to reason about logic in the code base with having many small dumb components, which didn't have any state or complex functionality. These would be controlled by a few smart parent components to coordinate them.

The result was a lot cleaner. We implemented this on a Web client, but I think the concept would work well in any codebase.... dumb classes are easier to understand


👤 Someone1234
Immutable JavaScript/CSS/Blobs/etc.

We have a very typical [web] codebase, server-side code (e.g. business rules, database access, etc), server-side Html generation, and JavaScript/CSS/Images/Fonts/etc stored elsewhere. Two repositories (content and code).

So the obvious question is: How do you manage deployment? Two repositories means two deployments, which means potential timing problems/issues/rollback difficulties.

The solution we use is painfully simple: We define the JavaScript/CSS/etc as immutable (cannot edit, cannot delete) and version it. If you want to bug fix example.js then it becomes example.js 1.0.1, 1.0.2, etc. You then need to re-point to the new version. The old versions will still exist and old/outdated references will continue to function.

This also allows our cache policy to be aggressive. We don't have to worry about browsers or intermediate proxies caching our resources for "too" long. We've never found editing files in-place, regardless of cache policy, to be reliable anyway. Some browsers seemingly ignore it (Chrome!).

We always deploy the "content" repository ahead of the "code" repository. But if we needed to rollback "code," it wouldn't matter because the old versions of "content" was never deleted or altered.

There's never a situation where we'd rollback "content" because you add, you don't edit or delete. If you added a bad version/bug, just up the version number and add the fix (or reference the older version until a fix is in "content," the old version will still be there).


👤 cbanek
Simplify, simplify, simplify. Don't make tomorrow's problem today's complexity.

Get rid of any configuration options that no one uses. These things get passed around in flags sometimes to deep levels and can make logic complicated. Don't add a configuration option until you are sitting at someone's desk and see they need it and why. Only add the bare minimum. Same for APIs, buttons, and features.


👤 tnolet
Don’t use Kubernetes or Microservices. Solves most problems.

Not even being sarcastic.


👤 weitzj
The biggest principles for emerging good code for me are:

Inversion of control (pass in your dependencies), keep your architecture orthogonal (make it composeable and really think if you need to inherit things rather than delegate them), code-generation of a transport api via gRPC and only focus on the business logic implementation.


👤 valand
Keep states where they are needed.

Make most things immutable.

Prefer composition to extension.

Treat Types as contracts.

Sandbox "unsafe" codes (codes that interacts with network, file storage, etc).

Eliminate side effects.

Eliminate premature abstractions.

Prefer explicit over implicit.

Keep components functional.

Prioritize semantic correctness and readability.

Use events to for inter-component communication when those components don't need to care about each other's functionality.

Think protocol over data.


👤 scarface74
Ripping out as much home grown code for cross cutting concerns (logging, database access, retry logic, etc) that previous developers used and using third party packages.

👤 hellwd
There are many decisions that you can make to improve the quality of your codebase. There is no a recipe that you can follow because each application is different but there are some general things that can make your life easier.

Here are some tips that helped me a lot:

- Keep your solution and tech-stack as simple as possible

- Mark those parts that can change often and try to make them configurable (when you have it configurable you don't need to change code and re-deploy every single adjustment)

- Make sure you have a good and readable logging

- Use DI

- Separate your application core application logic from the infrastructure part (DAL, Network Communication, Log Provider, File readers/parsers and similar)

- Keep your functions/methods clean and without side effects

- Method has to return something (try to minimize the usage of "void" methods)

- Split each feature or functionality you are working on into small pieces and compose the final thing with them

- Be disciplined about your naming conventions and code style


👤 tnolet
One of the things in the Clean Code book really helps.

Methods and functions should be around 5 lines.

Doesn’t always work but is great to aim for.


👤 pryelluw
Small and simple over big and complex. Plus some functional patterns and a lot of YAGNI based thinking.

👤 pearjuice
Honestly, tests and by extension testable code. The amount of enterprises processing tens to hundreds of millions of dollars (either business value or actual revenue) without tests of vital parts of their software is something which is mind blowing. You can sometimes not fathom how they are comfortable with changing a line without having tests to back them up. They f5 a page or recompile the server software, redeploy click through it and "yup it works let's ship" and then a few days later find out it broke a csv import of the external warehouse inventory system which runs once a week because they removed a dash between sku and title for better SEO in the online catalog. Oops, good luck finding out where the problem is because you have zero integration tests. A few million down the drain because import division couldn't possibly know what to forecast on due to no stock data. And this is not an exception to dumb bugs and malfunctions occurring because developers don't write tests.

You can start an entire business in consulting on test automation and you would never run out of work.


👤 oftenwrong
YAGNI, KISS

Choose Boring Technology http://boringtechnology.club/

Build your system to be level-triggered as much as possible. Its default mode should be reconciliation: examining its current state and transforming that into the desired state, especially if the current state is "something went wrong". Build in dumb reconciliation before worrying about making it more real-time.

The fewer moving parts, the better. Don't go multi-service architecture until you absolutely have to (see YAGNI, KISS).

Keep your business logic contained, separated from everything else, in ONE place. If I open up your business logic code, I shouldn't see anything about persistence, the network, etc. Similarly, I shouldn't find any business logic in your other concerns. The business logic interacts with other concerns via abstractions.

Be unforgiving when it comes to correctness guarantees. Use the type system as much as possible to make errors impossible.


👤 diehunde
Very basic ones:

- Strong test suite

- Delete duplication as much as possible by using any techniques such as method extraction and keeping classes and methods small.


👤 he0001
Reduce the number of tools, use them to the max, and know those tools intimately. When it falls short consider a new tool.

👤 happycoder97
I had been organizing all of my projects so far using layered architecture. Recently I read this article about layered architecture: https://dzone.com/articles/reevaluating-the-layered-architec... Now I feel that layered architecture was a poor choice for many of my previous projects.

So, I think, instead of layering, for example I should put everything that needs an access to a User entity's internal fields in User class itself.

For example: User.getProfileAsJson() // for sending out to frontend

Now I am confused regarding where to put methods that involves two entities. Suppose there is an Event entity which represents some online event that can be registered by the User.

Where is the best place to put getEventsRegisteredByUser()?


👤 matt_s
Specifically to allow easier changes: abstraction, encapsulation and separation of concerns.

An example would be if you have a module that calls a REST API to get/put something (say time sheets for your invoicing app), then have that be its own module that is testable.

Create internal TimeSheet data structures that you pass to/from that module. The core functionality of your app should be implemented using the TimeSheet data structures and you can have tests that use those and then separate tests around calling an API.

New customer comes along and says they want to send you CSV files via SFTP (yuck, but they got money). You just have to write a new interface that works with exchanging those files and gets them into your TimeSheet data structures, the core of your app should remain unchanged.


👤 srijanshetty
One controversial opinion: monorep which is ideal for small teams iterating really fast. The other one was figuring out 12 factor app by serendipity as we were focussing on keeping our operations simple.

👤 throwaway1954

👤 afpx

👤 ndreipoppa
For an event driven app(poker game) built with React, redux and redux-saga, we deleted almost the entire project(100k lines of code) because our logic was tightly coupled with the sagas and reducers. Now we moved our logic inside the state selectors(we use reselect), the reducers are dumb, while sagas are only used to listen/dispatch async actions.

👤 dustingetz
Clojure

👤 CameronBarre
Structuring software as a series of processes separated by queues in the small and large.