> But maybe it's done via a plugin system and spread over 5 different components, and there's actually two plugins you have to add, one for the repl, one for the internal handling, and there is non trivial communication between them. So you copy the 2 thousand lines of plugin code spread over 8 files and there's a bug. How do you debug it?

In enterprise Java, there is almost certainly a crazy amount of indirection, both static and compile-time.

A trivial task like "Write an API to retrieve a certain set of records from the database, and return them to the client caller" can involve:

1. Figuring out how the framework calls your API, and where the parameters are in the call. This is specified in some config (yaml/xml/whatever) file somewhere that the framework reads so that it can validate the parameters before it passes them to your code.

2. Figuring out the config-file syntax for specifying the validation criteria for each parameter and for the return value.

3. Determining if the specified list of columns is already encapsulated in an existing class. If it is, you're luck, just instantiate that class with the correct criteria and read the fields into the instance you will return to the framework.

4. If no existing class satisfies your needs (99 times out of a 100, there won't be), you realise by looking at similar existing API code that you need to create a application-logic level class that contains fields with all the values you require.

5. You do #5 above; then you read the existing API callchain again, and see that all other application-logic level classes don't talk to the DB directly anyway. They go through a DB-level class. This is because at some point in the future someone may want to swap out the underlying DB with another DB.

6. The DB-level class is tied to a specific ORM (say, MyBatis). It uses classes generated by MyBatis. You write your DB-level class to use a MyBatis-generated DAOClass.

7. You then dig into the details of the ORM, figure out which config-file (xml/yaml/whatever) to modify, how to write the template that contains the specific parameterised SQL statement (if using SQL), how to specify to the ORM that each column be matched to a specific Java type, and how to convert it if it is some other type.

8. You Aren't Done Yet! For each of those classes you created (the API handler, the Application-level class, the Dao class and maybe the generated classes from the ORM, you need to write unit-tests! The unit tests, in this simple example, will easily be twice as large as the actual new code written/generated.

9. You're done, for now, until integration testing in a pipeline fails. But that's a different issue that will always exist regardless of language or development methodology, so that one gets a pass.

And, believe it or not, the above is actually a simplified version of how it actually gets done. For example, there's going to be more lines of code mocking an instance in the tests than there are lines of code implementing the class itself.

Also, the framework calls your API, but your API probably doesn't directly instantiate the Application-level class - it hands it off to a 'call_handler' type of class that performs a threaded/sleep invocation[1] of an actual 'call' method in your API class.

Also, anything your class may need (instances of other classes that perform non-local stuff, like recording metrics, logging, lookups, etc) will be injected at runtime, and so you cannot simply declare an instance of the (for example) LookUpAddress class - you have to request an instance of that class from some sort of dependency mapper or injector, which will be provided to you when the framework calls into you.

This is normal. There are various reason for every single step above, and large companies (FAANGs, for example) will adhere to every single step (and more, like using classBuilders) because it checks all the boxes except velocity.

A startup, OTOH, had better forgo all of the above; create a monolith with no runtime injection or modification and skip all the unit tests (use more beefed up integration tests, for example). A startup can't afford to spend a day adding a single simple API, when, in the same time, they might be adding 25 new simple APIs.

There's no point in all of the rituals if the startup finds out 5 months in that the product doesn't have market-fit. It's better to determine market-fit in the first month or two.

Having paying customers is more important than having dependency injection, or the ability to switch databases.

[1] When not using Java, the 'call_handler' will use some sort of async/await call to do a proper async request.