ok cool, in the spirit of giving back, here is the one piece of advice I would give to avoid startups screwing themselves. I have seen this cost millions and millions in years 5-10.
Never assume that the incoming request will come from the web, and that your data store will be the same for all things (or stay the same over time for the same thing).
Every rapid MVC framework (Mean, Django, Rails, etc) encourages you to make those assumptions, by calling business logic operations coupled to the data persistence scheme (i.e. ORM calls), in the web controllers, which are your business logic units. Once you have hundreds or thousands of those, they are very, very hard to change. A good architecture looks like this:
- web controller calls your component management system to get a resource manager thing for dealing with certain domains, but it has no idea what this resource manager thing is, just what it does.
- it calls operations in your code (not library methods of 3rd party code!) on said thing to do stuff. it gets back some opaque thing it uses for a business operation, getting back some opaque response thing
- under the hood somewhere, there is an implementation file that knows this is SQL or whatever, but it only gets depended on and looked up by abstract interface - no client code knows how it works or exactly what it is on the inside.
To quote our beloved shaggy haired leader, components should say “I don’t know, I don’t care”. This is the core of onion/hexagonal/clean/ports & adapters in a nutshell.
Doing the above everywhere, all the time, requires a lot of discipline. You need to make a component manager that allows outer layers (web controllers, message bus endpoints, etc) to ask for the thing that gets them things while knowing nothing about how any of those are implemented. And you need to put operations on this for business actions (this is your “domain service layer” in DDD parlance). So to the early stage coder, this looks like heinous Java-esque overengineering - all these extra abstractions to write! But they are YOUR abstractions. Swapping out deps is trivial in your own code. Once you’ve done it, you literally change ONE FILE (the wiring in your component manager) to swap out the persistence scheme for a certain domain entity. And adding input to your system that comes from messages instead of web requests (async instead of sync) is now trivial. And everything is beautifully easy to test because changing components to mocks is easy peasy.
If your business succeeds, most problem domains will eventually need this (both changes to how you assumed you could get away with storing and querying and the assumption that all input would be synchronous http calls). To write it at the beginning, the overhead is someone writing like I dunno, a bunch of pages of extra code at the beginning and then handfuls more when they make a new domain resource? An extra two call layers when they use it? It’s just not that big. But if you don’t, it can be thousands and thousands of files that can’t be fixed while running the day to day operations. I got hired once to try and fix such a mess, and it couldn’t be done - the lunatics had run the asylum for too long, we would have need more extra firepower than the business was worth. Company got sold at a loss. I have done diligences on multiple companies in the same boat, mostly from a Ruby on Rails app that just grew. “You’re not going to need it” kills companies. It kills me when I hear that crap, just kills me. It is not pleasant talking to nice devs about their work while helping them realize the house is on fire and they should dust off the resume.
HTH!