Your Framework Should Not Own Your Business Logic

We have a habit of calling things non-functional requirements when we don’t want to think too hard about them.

Transactions. Retries. Rollbacks. These are often delegated to frameworks through annotations, configuration, or other runtime mechanisms. We treat them as infrastructure concerns, separate from the application itself.

I think this is a mistake.

Not because frameworks are bad, or because annotations are inherently wrong. The problem is that, by using them, we can unknowingly delegate decisions that belong to our application to a framework that has no business making them.

A transaction is not just an infrastructure concern

Consider a Spring application:

@Transactional
public void processOrder(Order order) {
    inventory.reserve(order);
    payment.charge(order);
    shipping.schedule(order);
}

This looks reasonable. The method performs a business operation, and Spring takes care of the transaction.

But what does taking care of the transaction actually mean?

  • Which operations must succeed or fail together for the order to remain consistent?
  • What happens if inventory is reserved but the payment fails?
  • Can we roll back the payment? Can we release the inventory?
  • What happens if shipping has already been scheduled when a later operation fails?
  • Which failures should trigger a rollback, and which should not?

These are not merely database questions. They are business questions.

A transaction defines a consistency boundary. It determines which state changes must be treated as one logical operation and what happens when that operation cannot be completed.

Putting @Transactional on a method does not answer these questions. It delegates the execution of a transaction to Spring, while the actual consistency requirements remain the application’s responsibility.

Worse, the annotation can make us believe that we have handled consistency when we have only configured one particular mechanism for it.

A database rollback, for example, cannot undo a payment processed by an external provider. The framework cannot infer the correct business response to that situation.

The application must own the decision. The framework should only help implement it.

The same problem applies to retries

Consider another familiar annotation:

@Retryable(maxAttempts = 3)
public void processPayment(Order order) {
    paymentGateway.charge(order);
}

Three attempts. Sounds reasonable.

But should we retry every failure? What if the payment provider processed the charge but the response was lost? Could another attempt charge the customer twice? Is the operation idempotent? Should a timeout be treated differently from a declined payment?

The correct retry policy depends on the operation’s semantics, its failure modes, and the guarantees provided by the external system.

Those are application-level concerns.

A retry annotation can execute a policy, but it cannot determine whether that policy is correct for the business operation. Even when the annotation is configured carefully, the retry mechanism can obscure the actual control flow and make it harder to reason about the application’s behavior.

This does not mean that retry mechanisms should be implemented from scratch. It means that the decision to retry, the conditions under which retries are allowed, and the consequences of repeated execution should be explicit and testable application behavior.

The framework may provide the mechanism. It should not silently become the owner of the policy.

Why do we call these non-functional requirements?

I suspect the terminology contributes to the problem.

We call performance, reliability, consistency, transaction management, and retry behavior non-functional because they do not describe the primary business outcome of an operation.

But this classification can be misleading.

Some concerns that are commonly grouped under non-functional requirements have direct consequences for functional correctness.

A payment processed twice is not merely a reliability issue. An order whose inventory has been reserved but whose payment has failed is not merely a transaction-management issue. A retry that repeats an irreversible operation is not merely an infrastructure issue.

These are failures of application behavior.

Of course, not every non-functional requirement is business logic. HTTP transport, database drivers, memory consumption, and deployment configuration are genuine infrastructure or operational concerns. But we should not use the label non-functional as an excuse to remove important decisions from the application code.

If a decision changes what the application does when something succeeds, fails, or is repeated, it deserves the same scrutiny as any other business rule.

Make the application testable without its framework

The application should be testable using plain Java tests, without starting Spring, a CDI container, or a database just to verify its decisions.

Consider a payment operation that should be retried only when a temporary failure occurs. Instead of delegating the entire policy to a @Retryable annotation, we can express it through a separate, reusable retry operation:

public static <T> Either<RuntimeException, T> retry(
        Supplier<T> operation,
        Class<? extends RuntimeException> retryOn,
        int maxAttempts) {

    RuntimeException lastError =
        new IllegalArgumentException("maxAttempts must be positive");

    for (int attempt = 0; maxAttempts > 0 && attempt < maxAttempts; attempt++) {
        Try<T> result = Try.of(operation::get);

        if (result.isSuccess()) {
            return Either.right(result.get());
        }

        Throwable cause = result.getCause();

        lastError = cause instanceof RuntimeException
            ? (RuntimeException) cause
            : new RuntimeException(cause);

        if (!retryOn.isInstance(lastError)) {
            break;
        }
    }

    return Either.left(lastError);
}

The business operation remains independent of the retry mechanism:

public Either<RuntimeException, PaymentResult> charge(Order order) {
    return retry(
        () -> paymentGateway.charge(order),
        TemporaryFailure.class,
        3
    );
}

Now we can test the retry policy independently, using a plain unit test and a fake payment gateway. We can verify that a temporary failure is retried, that success stops further attempts, and that the final failure is returned.

No Spring context. No proxies. No application server.

The example is deliberately simple; a real implementation must also consider idempotency, timeouts, backoff, and which failures are safe to retry. Those decisions depend on the operation and belong in its policy, not in a generic annotation applied without considering its semantics.

The point is that we should be able to test the application’s decisions independently of the mechanism that executes them.

Testing through the execution mechanism adds complexity on both sides: tests become harder to write and maintain, while the application inherits additional runtime behavior that can introduce overhead, hidden interactions, and bugs.

That complexity has a cost: development time, maintenance effort, and, ultimately, correctness. We should pay that cost only when the mechanism provides real value, not simply because our framework makes it convenient.

The same principle applies to transaction boundaries. We should be able to test which operations must succeed together and how failures are handled, while using integration tests to verify that the real database adapter correctly implements commits and rollbacks.

Does this mean we should eliminate frameworks?

Absolutely not.

An application does not run in a vacuum. It needs an environment, and that environment provides essential capabilities.

We are not going to write our own HTTP server just to receive user requests. We are not going to write a database driver. We do not need to implement dependency injection ourselves when a suitable container already exists.

Using Spring Boot, CDI, an application server, or another runtime is perfectly reasonable. So is using framework-specific adapters to connect the application to databases, message brokers, HTTP endpoints, and external services.

The distinction is where we draw the boundary.

I want the application to depend on abstractions that express the capabilities it needs, while the runtime supplies the implementations.

For example:

  • An HTTP adapter translates incoming requests into application calls.
  • A persistence adapter translates application operations into database queries.
  • A transaction adapter implements the transaction boundaries requested by the application.
  • A retry mechanism executes a retry policy that the application has explicitly defined.

The application should not need to know how an HTTP request is received or how a database connection is managed. But it should know which operations must be consistent, which failures are recoverable, and what should happen when an operation cannot be completed.

This is not about eliminating all framework dependencies from every line of code. It is about keeping framework-specific concerns at the edges, rather than allowing them to define the application’s behavior.

Framework independence is also about avoiding unnecessary complexity

There is another reason to care about this distinction: frameworks can introduce behavior that is not immediately visible in the code.

A method annotated with @Transactional may be intercepted by a proxy. Its behavior can depend on how the object was created, how the method was invoked, whether the invocation crossed the relevant proxy boundary, and how transaction propagation and rollback rules are configured.

Self-invocation, for example, can bypass proxy-based transaction interception in common Spring configurations. A method may look transactional in the source code while its actual execution does not behave as expected.

The issue is not that Spring is defective. These mechanisms have legitimate uses, and the framework documents their constraints.

The issue is that an implicit runtime mechanism introduces another layer of behavior that developers must understand and verify.

Sometimes that layer is justified. Sometimes a small, explicit piece of application code would be easier to reason about, easier to test, and less likely to conceal a mistake.

The more important a decision is to correctness, the less comfortable I am leaving it implicit.

Portability is a consequence, not the main goal

An application whose core behavior can be tested independently of its runtime is generally easier to move to another environment.

Perhaps today we use Spring Boot and tomorrow we need an application server. Perhaps we replace a database, change a message broker, or introduce a different external service.

If our application is tightly coupled to framework-specific annotations and runtime behavior, these changes can affect far more code than they should.

If the application expresses its requirements through ordinary code and clear interfaces, we can often replace the relevant adapters without rewriting the business logic.

But portability is not the primary goal.

I am not suggesting that every application should be designed to support five different frameworks or databases. That can become abstraction for abstraction’s sake.

The primary goal is to make application behavior explicit, understandable, and independently testable. Portability follows naturally from that discipline.

A practical rule

When deciding whether something belongs in the application or in a framework, I ask a simple question:

Can I verify that this behavior is correct using ordinary tests of the application itself, without starting its runtime?

If the answer is no, I investigate why.

Sometimes the behavior genuinely belongs to infrastructure, and an integration test is the appropriate way to verify it.

But sometimes the application has delegated a decision it should own to an annotation, a proxy, a container, or a framework default.

In that case, I try to bring the decision back into explicit application code and leave the framework responsible for providing the underlying mechanism.

This is not a ban on annotations. It is not a demand for hand-written infrastructure. And it is certainly not an argument for implementing everything ourselves.

It is a way of keeping responsibility where it belongs.

Your framework should help your application do its job. It should not quietly decide what that job is.

Choosing a Programming Language Is an Engineering Decision, Not a Matter of Taste

Performance, correctness, deployment, ecosystem, and maintenance: the objective criteria that matter more than personal preference.

Software engineers can spend an extraordinary amount of time debating programming languages. Rust versus Go. Java versus C++. TypeScript versus Elm. Functional programming versus object-oriented programming.

These discussions often revolve around personal preferences, isolated benchmarks, syntax, or the latest industry trends. Yet the most important question is rarely asked:

Which programming language minimizes the total cost and risk of building, deploying, and maintaining this particular system?

A programming language is a tool. Like any engineering tool, its value depends on the problem being solved, the constraints involved, and the people using it.

There are objective criteria for evaluating that value. Understanding them makes language selection a rational engineering decision rather than a matter of taste.

1. Runtime performance: measure the generated program, not the language’s reputation

The claim that C++ is fast, Java is slower, and Python is slow is a convenient oversimplification.

At the lowest level, computation comes down to machine instructions, register usage, memory access, cache locality, branching, and parallel execution. Programming languages are different ways of expressing computations, and their implementations translate those expressions into operations the hardware can execute.

For a given computational problem, different languages can produce machine code with comparable performance. The important distinction is not necessarily the performance they can achieve, but the effort required to achieve it.

Consider Java and C++.

A Java application running on a modern JVM can benefit from just-in-time compilation, method inlining, escape analysis, and other runtime optimizations. With an understanding of how the JIT compiler works, a developer can structure code so that the runtime produces highly optimized machine instructions.

In some workloads, the resulting performance can be comparable to an equivalent C++ implementation.

That does not mean the languages are universally equivalent in performance. Their memory models, runtime overhead, optimization opportunities, and constraints differ. It means that a language’s reputation is a poor substitute for measuring the actual system.

When evaluating performance, consider:

  • The computational workload and its algorithmic complexity.
  • The quality of the compiler, JIT compiler, or interpreter.
  • Memory layout, cache locality, and allocation behavior.
  • Vectorization, concurrency, and hardware acceleration.
  • Latency requirements, throughput, and memory consumption.

Benchmark representative workloads, not artificial examples that have little resemblance to production.

When the language really matters

Some constraints make particular language and runtime choices objectively unsuitable.

A small embedded device with limited RAM may not have room for a garbage collector and its associated runtime. A hard real-time system may not tolerate unpredictable pauses. A GPU kernel may require a specific programming model. A browser application must execute in an environment with defined compatibility constraints.

These are real engineering limitations, not stylistic preferences.

The right question is not which language is fastest in general, but which implementation can meet the system’s performance requirements within its resource constraints.

2. Correctness: how many bugs can you prevent before execution?

Performance gets attention because it is easy to compare in benchmarks. Correctness is often more important and considerably harder to quantify.

A defect discovered during development is generally cheaper to fix than one discovered in production. For small teams, this creates a particular challenge: the people writing the software may also be responsible for testing it, deploying it, supporting it, and fixing production incidents.

A language that catches more classes of mistakes at compile time can reduce the amount of verification work required elsewhere.

This is where languages such as Elm and Haskell become interesting.

Elm: making entire categories of mistakes harder to express

Elm is a statically typed functional language designed for front-end development. Its type system, exhaustive pattern matching, and explicit modeling of application state can make many common mistakes detectable during compilation.

For example, consider an application that represents a user account with several possible states:

  • Loading
  • Loaded successfully
  • Failed to load

A loosely modeled application might represent these states using several independent variables. That allows contradictory combinations: a request can be marked as both successful and failed, or the application can attempt to display data that does not exist.

Elm encourages developers to represent these alternatives explicitly using custom types and to handle them through pattern matching.

The compiler can then identify missing cases when the model changes.

Elm’s compiler also reports errors when code no longer conforms to the declared types. Its compiler is famously helpful when refactoring because changes to data structures and function signatures reveal many affected parts of the application.

This does not eliminate logical errors, incorrect business rules, or every runtime failure. It does make certain classes of mistakes harder to introduce unnoticed.

Haskell: stronger abstractions for expressing invariants

Haskell takes a similar philosophy further, offering algebraic data types, pattern matching, type classes, higher-order functions, and advanced type-system features.

These capabilities allow developers to encode more of a system’s intended behavior in its types.

For example, instead of representing an operation as a function that accepts arbitrary input and may fail in an unspecified way, a developer can model its valid inputs and possible outcomes explicitly.

Depending on the problem, types can express constraints around parsing, validation, state transitions, and error handling. The compiler can then enforce aspects of those constraints throughout the program.

The type system does not prove every business requirement correct by default. Nevertheless, a well-designed type model can prevent entire categories of invalid states and make assumptions visible in the code.

Why this matters for small teams

Imagine a project maintained by one developer or a team of two or three people, without dedicated QA engineers.

Every change carries a verification cost. Tests must be written, executed, interpreted, and maintained. Manual testing consumes time. Regression bugs can go unnoticed because there is nobody else routinely reviewing the system.

A language that shifts some of this verification into compilation can offer a structural advantage.

The benefit is not that testing becomes unnecessary. The benefit is that the compiler becomes another continuously available line of defense.

A useful way to think about it is:

The more correctness properties the language can enforce automatically, the less the team must rely exclusively on human discipline and runtime testing to preserve them.

For a small team, this can be a more valuable advantage than a modest improvement in execution speed.

3. Deployment: can the program run where it needs to run?

A language can be technically excellent and still be the wrong choice if deploying its applications is difficult or incompatible with the target environment.

Deployment constraints are concrete and often decisive.

For a browser application, relevant questions include:

  • Can the application run in the required browsers?
  • What JavaScript output, compatibility, or runtime support is required?
  • How large is the delivered application?
  • Are there restrictions on browser APIs or the execution environment?

For a backend service:

  • Is the language supported by the target hosting platform?
  • Does it require a particular application server or runtime?
  • How easy is it to build, package, deploy, and roll back?
  • Can the application run in the available containers or serverless environment?
  • What operating-system and architecture dependencies exist?

For mobile and embedded applications, hardware support, platform APIs, binary size, memory usage, and toolchain availability can be even more important.

These constraints should be evaluated before selecting a language, not after the application has been built.

A deployment model that works perfectly in a developer’s environment but requires substantial additional infrastructure in production introduces cost and operational risk.

4. Ecosystem maturity: how much infrastructure must you build yourself?

A language’s ecosystem determines how much existing engineering work you can reuse.

Consider the availability and quality of:

  • Libraries for required integrations and protocols.
  • Frameworks and established architectural patterns.
  • Database drivers and migration tools.
  • Testing, debugging, profiling, and observability tools.
  • Security updates and maintained dependencies.
  • Documentation and practical examples.

A mature ecosystem can significantly reduce development time and the amount of custom code that must be maintained.

However, the total number of packages is not a useful metric on its own. A smaller ecosystem may be entirely adequate for a specialized application, while a large ecosystem may still lack a critical integration.

The objective question is whether the language and its ecosystem provide the capabilities the project actually needs, at an acceptable maintenance and security cost.

5. Developer availability: how expensive is it to build and maintain the team?

Hiring and knowledge transfer are real engineering costs.

A language with a large pool of experienced developers is usually easier to staff. A niche language may require more recruitment effort, longer onboarding, or reliance on a small number of specialists.

The implications extend beyond hiring. Consider what happens if the original developer leaves, the project needs urgent support, or a new team must take ownership.

But popularity should not automatically override every other criterion.

For a small, stable application maintained by one experienced developer, a language that substantially improves correctness or productivity may be worth its smaller talent pool.

For a large organization with frequent team changes, broad hiring availability may carry much more weight.

The correct decision depends on the project’s expected lifetime, staffing model, and operational risk.

6. Maintainability: optimize for the lifetime of the system

Writing code is only one part of software development. Reading, modifying, debugging, and extending existing code can consume far more effort over the lifetime of a system.

Maintainability depends on several factors:

  • How clearly the language expresses the domain model.
  • Whether the type system makes invalid states difficult to represent.
  • How safely the code can be refactored.
  • Whether the compiler identifies the consequences of changing interfaces.
  • How easily developers can understand data flow and error handling.
  • Whether the ecosystem provides stable, well-supported abstractions.

A language that allows a developer to write an initial implementation quickly may not minimize the cost of maintaining it for ten years.

Conversely, a language with a steeper learning curve may pay for itself if it consistently reduces debugging and refactoring effort.

This is one reason functional programming and strong static typing deserve consideration even when they are not the dominant choice in a particular market.

The relevant metric is not how quickly someone can write the first version. It is how much effort is required to keep the software correct as requirements evolve.

7. Productivity: measure the whole development process

Productivity is frequently used to justify subjective language preferences. One developer says a language is more productive because its syntax is concise. Another prefers a language because its abstractions feel natural.

Neither statement is sufficient on its own.

A more useful assessment considers the complete development cycle:

  1. How long does it take to implement a feature?
  2. How much code must be written and maintained?
  3. How many defects are introduced?
  4. How difficult are debugging and refactoring?
  5. How much testing and manual verification are necessary?
  6. How expensive is deployment and ongoing operation?

Concise syntax can help, but it does not automatically translate into fewer defects or lower maintenance costs.

Similarly, a strong type system can impose some upfront modeling effort while reducing the cost of subsequent changes.

For a one-person project, these trade-offs can be particularly significant. The developer has limited time and cannot compensate indefinitely for weak tooling or a language model that makes mistakes difficult to detect.

8. A practical framework for choosing a language

Instead of asking which language is best, start by identifying the project’s constraints and assigning priorities to the relevant criteria.

CriterionObjective question
PerformanceDoes it meet measured latency, throughput, and resource requirements?
CorrectnessWhich classes of errors can be detected before runtime?
DeploymentDoes it run reliably in the required target environment?
EcosystemAre the necessary integrations and tools available and maintained?
Team availabilityCan the system be staffed and supported over its lifetime?
MaintainabilityHow expensive are debugging, refactoring, and future changes?
ProductivityWhat is the total cost of implementing and operating the system?
Operational riskCan the team diagnose, deploy, recover, and support the application reliably?

Not every criterion has equal importance for every project.

For a small embedded controller, memory usage and timing guarantees may dominate the decision.

For a browser application maintained by one developer, deployment compatibility and compile-time correctness may be more important than raw execution speed.

For a high-throughput numerical workload, benchmark results and hardware utilization may be decisive.

For an enterprise system expected to survive multiple generations of developers, ecosystem maturity, hiring, and maintainability may outweigh nearly everything else.

The goal is not to assign every language a universal score. It is to identify the constraints that matter, eliminate unsuitable options, and compare the remaining candidates against the same requirements.

9. Where personal preference fits

Personal preference is not entirely irrelevant. Familiarity with a language can reduce onboarding costs, and developers who understand a language deeply are often more productive with it.

But familiarity should be treated as an economic consideration, not as proof that the language is technically superior.

A preference becomes an engineering argument when it can be connected to a measurable outcome: fewer defects, shorter delivery times, easier hiring, simpler deployment, or lower maintenance costs.

Statements such as “I prefer the syntax,” “this language feels cleaner,” or “everyone uses it” may be useful starting points for discussion. They are not sufficient grounds for a major architectural decision.

The same applies to fashionable claims about performance. A benchmark showing that one language wins a particular microbenchmark does not establish that it is the right choice for an application with different workloads and constraints.

Engineering decisions require evidence relevant to the system being built.

Conclusion: choose the tool that minimizes total engineering cost

Programming languages are not interchangeable in every practical situation. Their runtimes, type systems, deployment models, ecosystems, and hardware constraints create meaningful differences.

But those differences should be evaluated in terms of the outcomes they produce.

A language that generates exceptionally fast code may be unsuitable for a memory-constrained embedded device. A language with a smaller ecosystem may be ideal for a specialized application. A strongly typed functional language may be particularly valuable for a small team that needs to minimize the risk of regressions without dedicated QA resources.

There is no universally best programming language. There are languages that are better or worse suited to a specific set of requirements.

The most useful question is therefore not:

Which programming language do I like best?

It is:

Which language gives this team the best combination of performance, correctness, deployment simplicity, and long-term maintainability under the constraints of this project?

That is an engineering decision. And like any good engineering decision, it should be justified by evidence rather than preference.

How We Stopped a Legacy JBoss System from Slowing Down Delivery

At bitGloss, we often work with systems that businesses still depend on heavily, but that very few teams still fully understand.

One common pattern we encounter is enterprise infrastructure that was designed for a very different era of software engineering: large application servers, highly abstracted deployment models, and operational complexity that slowly becomes impossible to justify.

Recently, we helped a client modernize an aging distributed JBoss environment running on AWS.

The Problem: Enterprise Complexity Without Enterprise Benefits

The client’s platform relied on a clustered JBoss architecture that had gradually turned into a maintenance bottleneck.

The environment:

  • Complex XML-based server configuration
  • Custom classloader hierarchies
  • Specialized deployment descriptors
  • Distributed session replication setup
  • Cluster coordination management
  • JBoss-specific operational knowledge

In theory, these application server capabilities offered flexibility and scalability. In practice, most of the advanced enterprise features were barely used.

What remained was mostly operational overhead.

Deployments were slow and fragile. Memory usage was unnecessarily high. Troubleshooting required specialized knowledge that fewer engineers still possess today. Even simple upgrades became risky because the platform depended on behaviors tightly coupled to the JBoss runtime.

The infrastructure had effectively become legacy middleware.

The Real Cost of Legacy Middleware

This is a pattern we see frequently in older enterprise systems.

The issue usually isn’t that the application itself is obsolete. Often, the business logic is still valuable and stable.

The real problem is the operational surface area surrounding it.

Older enterprise stacks tend to accumulate:

  • Vendor-specific deployment models
  • Overengineered runtime abstractions
  • Fragile configuration layers
  • Tooling that fewer engineers understand each year
  • Infrastructure dependencies that slow down delivery

Over time, the organization ends up spending more effort maintaining the platform than improving the product.

The Migration Strategy

Rather than attempting a risky full rewrite, we focused on reducing infrastructure complexity while preserving application behavior.

We migrated the platform from JBoss to lightweight Tomcat instances.

The migration included:

  • Refactoring deployment descriptors to standard servlet specifications
  • Eliminating complex EAR packaging
  • Simplifying deployments to standard WAR artifacts
  • Removing unnecessary application server dependencies
  • Reducing runtime-specific configuration requirements

The goal was not simply “moving servers.”

The goal was operational simplification.

The Outcome

The results were immediate and measurable:

  • Deployment times reduced by 75%
  • Memory footprint reduced by 40%
  • Operational troubleshooting became dramatically simpler
  • Infrastructure became easier to automate and maintain
  • The client was no longer dependent on increasingly rare JBoss expertise

Most importantly, the operations team could stop fighting middleware and focus again on delivering business value.

Legacy Modernization Does Not Always Mean Rewriting Everything

Many organizations assume modernization requires rebuilding entire systems from scratch.

In reality, some of the highest-impact improvements come from simplifying the infrastructure layers around existing applications.

Replacing heavyweight legacy middleware with lightweight, standardized components can significantly reduce operational risk, infrastructure cost, and maintenance burden — without disrupting core business functionality.

That is often where the fastest return on investment exists.

At bitGloss, we specialize in exactly these kinds of legacy modernization projects: untangling complex systems, reducing operational friction, and helping businesses regain control over infrastructure that has become difficult to evolve.

Is Your Website Going Down Right When You Need It Most?

There are few things more damaging to a business than an application that becomes unresponsive exactly when traffic peaks. Not during quiet hours, not in the middle of the night — but during a product launch, a marketing campaign, or the busiest trading period of the year. The timing feels cruel, but it’s not a coincidence. Peak load is precisely when hidden architectural problems surface.

This is the story of one such system, and what was actually causing it to fail.

The architecture that looked fine on paper

The infrastructure in question was built on Azure and, by most measures, looked solid. Redis was in place as a caching layer to reduce database pressure. The database itself was properly indexed. Servers had comfortable headroom on both CPU and memory — no resource exhaustion, no obvious bottlenecks. On a quiet afternoon, everything worked beautifully. Response times sat around 200ms, well within acceptable range.

Then peak traffic would arrive. Response times would climb from 200ms to 2 seconds, then 5, then 10. Eventually the application would stop responding altogether. And then, as traffic subsided, it would recover on its own — as if nothing had happened.

This pattern is particularly disorienting for engineering teams. The system heals itself, so there’s no crash to investigate, no error log with a clear smoking gun. Just a recurring window of failure that’s hard to reproduce and even harder to explain.

Why the obvious suspects weren’t guilty

The natural instinct when an application slows under load is to look at the most visible resources: CPU, memory, database query times. All of them were fine. Redis, the caching layer specifically designed to handle this kind of load, was responding in microseconds. The database wasn’t under unusual pressure. The servers weren’t breaking a sweat.

This is where many investigations stall. If the database is fine, Redis is fine, and the servers have headroom — what’s left?

The answer was in a place most teams don’t think to look: thread pool metrics and connection pool utilization.

The actual problem: threads waiting for nothing

Modern web applications handle concurrent requests using thread pools — a fixed set of worker threads that process incoming requests. When a request comes in, it gets assigned to an available thread. If all threads are busy, the request waits in a queue.

The application was running with default thread pool settings. Those defaults are reasonable for low-to-moderate traffic, but they set a relatively low ceiling on how many threads are available at any given moment. Under normal load, there were always enough threads to go around. Under peak load, every thread was occupied — not doing heavy work, but waiting. Waiting to make a call to Redis. Waiting for a Redis response that would arrive in microseconds.

Here’s the paradox: Redis was fast. The problem wasn’t Redis performance. The problem was that the threads making Redis calls were blocking while they waited, even for those microseconds, and there weren’t enough of them to keep up with the incoming request volume. Requests piled up in the queue. Response times climbed. Eventually the queue filled and the application became unresponsive.

The infrastructure was fine. The bottleneck was a configuration default that nobody had revisited since the system was first deployed.

What the fix looked like

The solution involved three targeted changes, none of which required re-architecting the system.

Thread pool reconfiguration. We analyzed the expected concurrent load and pre-allocated a sufficient number of worker threads to handle peak traffic without queuing. This meant the application could process many more simultaneous requests without threads blocking each other.

Proper connection pooling for Redis. Related to the thread problem was how connections to Redis were being managed. Without a proper connection pool, the application was creating and tearing down Redis connections more frequently than necessary, adding latency and overhead to every cache interaction. A well-configured connection pool meant connections were reused efficiently, and Redis calls became as fast as they should have been all along.

Monitoring for thread pool utilization. Perhaps as importantly as fixing the immediate problem, we added visibility into thread pool metrics going forward. CPU and memory graphs are standard in most monitoring setups. Thread pool saturation almost never is — which is exactly why this problem had gone undetected for so long. If thread pool utilization starts climbing toward its ceiling, the team now knows before users feel it.

The results

Response times stabilized at under 300ms even during peak traffic periods. The infrastructure was able to handle five times the previous concurrent load without degradation. The underlying hardware, the database, and Redis itself didn’t change. Only the configuration did.

What this means for your team

If your application behaves well under normal conditions but degrades or fails under peak load, the problem is almost certainly not the thing you’re measuring most. CPU and memory are easy to monitor, so teams watch them closely. Thread pools, connection pools, and queue depths are harder to instrument, so they go unmonitored — and that’s precisely where these failure modes hide.

Before reaching for more servers or a bigger cache layer, it’s worth asking: do we actually know what’s happening inside our application at the thread level during peak load? In most cases, the answer is no. And in many cases, that’s where the answer is.

Infrastructure problems are rarely about infrastructure. They’re about configuration, visibility, and knowing where to look.


At Bitgloss, we help engineering teams find the real cause of performance failures — not just the obvious suspects. If your application is struggling under load, get in touch.


Is Your Database Slow? Probably Not.

Nine times out of ten, when an engineering team starts complaining about “slow database performance,” the database itself is perfectly fine. The real culprit is almost always hiding somewhere else — in the way queries are written, in how the application accesses data, or in decisions that made sense at the time but were never revisited as the system grew.

This distinction matters more than it might seem. If you assume the database is the problem, you start looking at hardware upgrades, migration to a “faster” database engine, or expensive infrastructure changes. All of that costs time and money — and probably none of it will fix the actual issue.

The symptom everyone misreads

A slow application feels like a slow database. A page that takes five seconds to load, a report that times out, an API endpoint that keeps failing under load — these all point fingers at the database layer. But the database is usually just doing exactly what it was asked to do. The problem is what it was asked to do.

We’ve seen queries that took literal minutes to complete, not because the server was underpowered, but because they were missing proper indexes, performing full table scans on millions of rows, or doing complex joins in the wrong order. In one real case, a reporting query was joining five tables with no indexes on any of the join columns — and then passing the entire result set back to the application to be filtered in code. The database was faithfully returning millions of rows that would eventually be narrowed down to a few hundred. It was working extremely hard to accomplish something that should have been trivial.

What’s actually going wrong

There are a handful of patterns that cause the vast majority of database performance problems:

Missing indexes on join and filter columns. When a query runs a WHERE clause or a JOIN on a column with no index, the database has no choice but to scan the entire table to find matching rows. On a table with a few thousand rows, this is fast enough that nobody notices. On a table with a few million rows, it becomes a bottleneck that grows worse every week as data accumulates.

Filtering in application code instead of in the database. This happens when developers pull a large dataset and then loop through it in the application to find what they need. The database does more work than necessary, more data travels across the network, and the application spends time on logic the database could handle in a fraction of a second.

N+1 query patterns. This is one of the most common and most damaging patterns, especially in applications that use ORMs. Instead of fetching related data in a single query, the application fires one query to get a list of records, then fires a separate query for each record to get its related data. Fetch a list of 200 orders? That’s 201 database queries. At scale, this silently destroys performance.

Joins ordered by convenience rather than efficiency. The order in which tables are joined can dramatically affect how much work the database needs to do. Starting a join with a large, unfiltered table and narrowing down later means the database is carrying a heavy load through most of the operation. Reordering joins so that the most selective conditions are applied early can cut execution time significantly.

What a proper fix looks like

When we analyzed the execution plans on the slow queries described above, the path forward became clear quickly. Execution plans show you exactly how the database is processing a query — which indexes it’s using (or not using), how many rows it’s scanning at each step, and where the time is actually being spent. Most teams never look at them.

The changes we made were not dramatic. We added indexes strategically on the columns used in WHERE clauses and JOIN conditions. We rewrote the join order to match the actual selectivity of each table. We moved filtering logic from application code into proper WHERE clauses. And we replaced N+1 patterns with proper JOINs or batch fetches.

The results were. Queries that previously took two to three minutes now complete in under 100 milliseconds. The database hardware didn’t change. The database engine didn’t change. Only the way the queries were written changed.

What this means for your team

If your application is feeling sluggish and the database is getting the blame, the first step is not to panic and not to reach for the infrastructure budget. The first step is to look at what your application is actually asking the database to do.

Pull the slowest queries from your logs. Look at their execution plans. Check whether the columns you’re filtering and joining on have indexes. Look for N+1 patterns in your ORM queries. These are not exotic problems — they are extremely common, and they are fixable without rewriting your application or migrating to a new database.

The database is rarely the bottleneck. It’s just being asked to do things inefficiently.


At bitGloss, we help engineering teams diagnose and fix exactly these kinds of problems — turning slow, expensive queries into fast, predictable ones without unnecessary infrastructure changes. If your application is struggling with database performance, get in touch.


De la înființarea firmei la facturarea clienților – pași practici


1. Deschiderea unei firme în România

Dacă vrei să începi să lucrezi legal cu clienți, trebuie să alegi o formă juridică. Cele mai comune sunt:

  • PFA (Persoană Fizică Autorizată) – simplu de deschis, costuri reduse, dar răspunderea este personală.
  • SRL (Societate cu Răspundere Limitată) – implică mai multă birocrație, însă oferă protecție patrimonială și credibilitate în fața clienților.

Pașii principali pentru un SRL:

  • Alegerea și rezervarea denumirii firmei la ONRC.
  • Stabilirea sediului social (poate fi și la domiciliu).
  • Redactarea actului constitutiv.
  • Depunerea capitalului social minim (200 lei).
  • Înregistrarea la Registrul Comerțului și obținerea certificatului de înregistrare (CUI).
  • Înregistrarea opțională în scopuri de TVA, dacă e necesar.
  • Deschiderea unui cont bancar pentru firmă – obligatoriu pentru SRL, recomandat și pentru PFA. Contul va fi folosit pentru tranzacții și încasarea facturilor.

Mai multe detalii oficiale găsești pe site-ul ONRC – Înmatriculări societăți.


2. Ținerea evidenței orelor lucrate (exemplu simplu în Excel)

Un mod clasic de a urmări timpul lucrat este să folosești un tabel în Excel.

Exemplu de structură:

DataClientProiectActivitateOre lucrateTarif/orăTotal
01.08.2025Client AWebsiteDezvoltare frontend5150750
02.08.2025Client BConsultanțăAnaliză procese interne3200600

Poți găsi modele gratuite de foi de pontaj pe site-ul Microsoft Office Templates.


3. Facturarea manuală în sistemul e-Factura

Din 2024, firmele din România trebuie să trimită facturile către clienți prin RO e-Factura. Sistemul funcționează pe baza unui fișier XML conform standardului european UBL 2.1.

Procesul manual:

  1. Creezi factura în Excel/Word.
  2. Construiești manual un fișier XML conform specificațiilor ANAF.
  3. Te loghezi în Spațiul Privat Virtual (SPV).
  4. Încarci fișierul XML.
  5. Aștepți validarea. În caz de erori, refaci factura și retrimiți.

Este un proces posibil, dar consumator de timp și predispus la greșeli tehnice.


4. Cum simplifică worklog.ro procesul

worklog.ro este o platformă gândită special pentru freelanceri și mici afaceri din România, care își facturează timpul și serviciile.

Beneficii:

  • Logarea simplă a orelor – în loc de Excel, introduci activitățile direct pe clienți și proiecte.
  • Rapoarte lunare instant – afli exact câte ore ai lucrat și cât trebuie facturat.
  • Facturi conforme e-Factura – platforma generează automat fișierul XML conform standardelor ANAF, gata de încărcat în SPV.
  • Reducere erori – nu mai scrii manual coduri XML.
  • Economisești timp – un proces care altfel dura ore devine o chestiune de câteva minute.

Concluzie

Deschiderea unei firme și facturarea clienților implică pași clari și proceduri oficiale. Deși Excel și crearea manuală a XML-urilor sunt soluții de început, ele devin rapid complicate și ineficiente.

Prin worklog.ro, antreprenorii și freelancerii își pot ușura munca: loghează orele direct în platformă, generează facturi conforme e-Factura și scapă de stresul birocrației.


Resurse utile