AI Agent Memory: Build vs Buy for Enterprise Teams

Every AI team eventually hits this question

Your agents need persistent memory. That’s settled. The question engineering leaders are now asking is: do we build the memory infrastructure ourselves, or buy a managed solution?

This is not a simple question. The answer changes dramatically based on your team size, compliance posture, and time-to-market pressure. This post gives you the honest framework to make that call — not the answer designed to sell you something.

(Full disclosure: we build Trace Continuity. We’ll tell you when building makes more sense.)

The problem: AI memory without governance is a liability

Before the build vs. buy decision, there’s a framing decision that most teams get wrong.

The question is not “do we need AI memory?” You do. The question is “do we need governed AI memory?”

In a regulated environment, the answer is yes — and governed memory is meaningfully harder to build than plain memory.

Here’s what “governed AI memory” actually requires in production:

  • PII auto-redaction before anything reaches storage
  • Retention policies enforced at the infrastructure layer
  • Immutable audit logs for every read, write, and delete
  • Multi-tenant isolation enforced architecturally
  • Deletion workflows with proof-of-deletion for GDPR Article 17 and CCPA compliance
  • Access control scoped per memory, per agent role

If your AI agents touch patient data, financial records, legal documents, or employee information — that entire list is required.

The build path: what it actually costs

The minimum viable memory layer (2-4 weeks)

A basic memory layer — embed, store, retrieve — is genuinely not that hard. A vector store (pgvector, Pinecone, Weaviate), an embedding pipeline, a retrieval API. An experienced engineer can have this running in two weeks.

This is the part teams budget for. It’s not the expensive part.

Adding governance (3-6 months)

Once the basic layer works, the questions start arriving:

  • “How do we enforce data retention? HIPAA says we can’t hold PHI longer than clinically necessary.”
  • “Which agents can access which memories?”
  • “Our compliance team needs an audit log.”
  • “A user exercised GDPR right to erasure. Can we prove we deleted everything?”
  • “PII is leaking into the vector store.”

Each of those is a separate engineering project. Realistically: a team of 2-3 engineers, 6-12 months, before you have something you’d put in front of an auditor.

Ongoing maintenance burden

The build cost is not one-time. Governance infrastructure requires:

  • Staying current on regulatory changes
  • Responding to security incidents and CVEs
  • Building tooling for compliance reporting
  • Supporting deletion workflows

The buy path: what a managed solution actually provides

Governance as infrastructure, not application code

With a managed solution like Trace Continuity, the governance layer is not something your developers implement on top of the memory store. It is the memory store.

// Every write passes through: PII scan -> redact -> TTL-enforce -> access-control -> audit-log
await memory.remember({
  agent: "intake-bot",
  tenant: "acme-corp",
  fact: "Patient prefers morning appointments. DOB: 1978-04-15.",
  retention: "365d",
  access: ["clinical-ops"]
});
// Stored: "Patient prefers morning appointments. DOB: [REDACTED]."
// Redaction event logged. TTL set. Access policy stored. Audit record created.

What “managed” means for compliance

Requirement Build-it-yourself Managed solution
PII redaction You build detection pipeline Pre-storage, 15+ PII types, audit log
Retention enforcement Cron jobs, your logic, your bugs Infrastructure-layer TTL, automatic
Audit logs You design the schema and queries Queryable by agent/tenant/time, exportable
GDPR deletion proof Manual workflow, hope it works forget() with immutable proof of deletion
Multi-tenant isolation Namespace conventions, developer discipline Architectural enforcement, 403 on mismatch
Access control API key scoping Per-memory, per-agent-role policies

Compliance certifications you don’t have to earn

SOC 2 Type II and HIPAA BAA are table stakes for enterprise sales. Earning SOC 2 Type II in-house requires 6-12 months of audit preparation. A managed solution transfers that burden.

The decision framework

Build if:

  • Your compliance requirements are zero or negligible
  • You have a genuinely differentiated memory architecture
  • Your team has available engineering capacity and a long runway (3-5 engineers, 12+ months)

Buy if:

  • You’re in a regulated industry (healthcare, fintech, HR tech, legal, insurance)
  • Enterprise deals require compliance documentation
  • Time-to-market is a constraint
  • You’re a startup or growth-stage company

The mistake most teams make

Teams underscope the build. They plan for the vector store and the retrieval API — the 2-4 week project. Then governance lands on the roadmap mid-build and pushes the delivery date by 6 months.

If you’re going to build, scope the governance from day one.

If you’re going to buy, buy early. The cost of running ungoverned memory in a regulated environment while the build project runs over deadline is not just engineering time. It’s liability.

What Trace Continuity provides

Trace Continuity is governed AI memory infrastructure for teams that need to move fast without accumulating compliance debt.

  • REST API for writing, reading, and governing agent memory
  • PII auto-redaction before storage, 15+ types out of the box
  • Retention policies enforced at the infrastructure layer
  • Immutable audit logs for every memory operation
  • Multi-tenant isolation enforced architecturally
  • GDPR/CCPA-compatible deletion workflows with proof

Free tier available. No credit card required.

Read the API documentation
See pricing

Further reading

2026 LG OLED TV Serisi Ön Siparişe Açıldı

LG Electronics, CES 2026’da tanıttığı yeni nesil OLED TV serisini Türkiye’de ön siparişe açtı. OLED kategorisindeki 13 yıllık uzmanlığını yeni nesil OLED evo teknolojileriyle ileri taşıyan LG; gelişmiş görüntü performansı, yapay zekâ destekli kullanıcı deneyimi ve premium tasarım yaklaşımıyla dikkat çeken 2026 OLED TV ailesini lansmana özel avantajlarla kullanıcılarla buluşturuyor. Yeni nesil LG OLED evo modelleri; daha yüksek parlaklık seviyesi, gelişmiş renk doğruluğu ve derin kontrast performansıyla daha etkileyici bir izleme deneyimi sunuyor. Hyper Radiant Color Technology ve yeni nesil Alpha 11 AI işlemci sayesinde içerikler; sahneye, ortam ışığına ve kullanım alışkanlıklarına göre gerçek zamanlı optimize ediliyor. Gelişmiş yapay zekâ mimarisi ise görüntü netliği, doku koruması ve ses performansını birlikte değerlendirerek daha doğal ve sürükleyici bir deneyim oluşturuyor. Yüksek yenileme hızları, ultra düşük tepki süresi ve gelişmiş oyun özellikleriyle 2026 LG OLED serisi; sinema, spor ve oyun deneyimini bir üst seviyeye taşıyor. Kendi kendine ışık yayan piksel teknolojisiyle OLED kategorisinin […]

2026 LG OLED TV Serisi Ön Siparişe Açıldı yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

AI Memory Governance for Legal Tech: How Contract AI Agents Handle Privileged Data

The problem: legal AI agents process data that cannot be mixed

A litigation firm deploys an AI agent to help associates review discovery documents. The agent needs to remember which documents have been analyzed, which privilege log decisions were made, and what matters still need review. This is a legitimate use case — the agent should build context across sessions.

But the discovery documents contain privileged communications between attorneys and clients. When the same AI agent is deployed for a different matter, it must not retain any memory of the first matter. And when the client requests their file in discovery, every access to their data — including AI memory retrieval — must be logged in a way that survives attorney-client privilege scrutiny.

This is the core tension in legal AI memory: the same properties that make AI memory useful (persistent, cross-session, context-building) are the properties that create privilege exposure and compliance risk.

Contract AI agents (contract review, redline comparison, obligation tracking) face similar constraints. A contract agent working on multiple M&A deals cannot remember deal terms from Deal A when working on Deal B. An IP due diligence agent reviewing patent portfolios for Buyer A cannot surface knowledge from Buyer B’s portfolio.

Why generic memory stores fail for legal tech

No multi-tenant isolation at the data layer

Standard memory stores treat all memories as equivalent. For a law firm running a single AI agent across multiple client matters, this means:

  • Matter A’s strategy discussion surfaces in Matter B’s context — privilege crossover
  • Client C’s document review history is available to the agent when it switches to Client D’s matter — conflict of interest
  • A privilege log decision made in Matter A can unconsciously influence the agent’s analysis in Matter B

Multi-tenant isolation at the query level is not enough. The memory store must architecturally separate data at the tenant + matter scope.

No privilege boundary enforcement

Most memory systems have no concept of privileged vs. non-privileged access. Legal AI memory needs:

  • PII detection — client names, matter numbers, document IDs tokenized before storage
  • Deterministic matching — the same client or matter identifier always produces the same token
  • Audit logging — every detection event logged with PII type, token prefix, and timestamp

No discovery-ready audit trail

When opposing counsel requests production of AI system logs in litigation, most memory solutions cannot answer basic questions: What did the AI agent see? When did it access it? Who authorized that access?

How governed memory solves legal AI data challenges

Matter-level isolation with no crossover

When a legal AI agent writes a memory, the memory is scoped to a matter identifier. Matter A’s memories are only accessible when the agent is actively operating in Matter A’s context.

// Write memory scoped to a specific legal matter
const response = await fetch("https://tracecontinuity.com/v1/memories", {
  method: "POST",
  headers: {
    "Authorization": "Bearer mnm_your_api_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    agent: "m-and-a-contract-review",
    content: "Matter ACME-2024-089: Anti-sandbagging clause identified in Section 8.3. Target counsel flagged as resistant to MAC clause.",
    retention: "180d",
    scope: "matter:ACME-2024-089"
  })
});
// In a different matter session — ACME-2024-089 memories are not retrieved
// This is architecturally enforced — not a convention

Deterministic tokenization for attorney-client privilege

Documents reviewed by legal AI agents often contain client names, case references, attorney work product, and privileged communications.

const crypto = require("crypto");
function tokenizeMatterId(value, secretKey) {
  const normalized = value.toString().trim().toUpperCase();
  const hmac = crypto.createHmac("sha256", secretKey);
  hmac.update("MATTER:" + normalized);
  return "MATTER_TOKEN_" + hmac.digest("hex").substring(0, 8);
}

// Session 1: Matter ACME-2024-089 enters the agent context
const token1 = tokenizeMatterId('ACME-2024-089', process.env.TOKENIZATION_KEY);
// -> "MATTER_TOKEN_f7a2c901"

// Session 2: Three weeks later, same matter identifier
const token2 = tokenizeMatterId('ACME-2024-089', process.env.TOKENIZATION_KEY);
// -> "MATTER_TOKEN_f7a2c901" (identical — deterministic)
console.log(token1 === token2); // true

This approach means the memory database contains tokens — not matter identifiers. In a document production request, the data produced contains no privileged client identifiers.

Audit trail for privilege log compliance

Every memory write, read, and deletion in Trace Continuity is logged to a governance_events table. For legal AI deployments, compliance officers can query:

  • What client data did the AI agent access, and when?
  • Were any PII types detected and redacted during this session?
  • When did retention policies trigger — and what was deleted?
# Query usage endpoint for audit data
curl -X GET "https://tracecontinuity.com/v1/usage" 
  -H "Authorization: Bearer mnm_your_admin_key"
# Response includes:
# {
#   "total_memories": 3201,
#   "memories_pii_redacted": 847,
#   "governance_events": 3841
# }

Legal tech compliance requirements in context

Requirement What governed memory provides
Attorney-client privilege PII/tokenization; no raw privileged data in storage
Client matter isolation Matter-scoped memory retrieval — architecturally enforced
Discovery of AI system logs Immutable governance_events audit trail
Data retention (matter closure) Retention policies tied to matter duration
Work product protection Agent reasoning and document analysis tokenized before storage
State bar ethics compliance AI memory decisions auditable

For law firms deploying AI agents under ABA Model Rules of Professional Conduct, the key requirement is the ability to demonstrate that AI-assisted work was conducted with appropriate safeguards.

Try it in the playground

The Playground lets you test matter-scoped memory isolation, PII detection on legal document text, and tokenization in real time.

Two tables, zero migrations, full LINQ — a .NET data engine that’s been running our production for 3 months

Today I’d like to slow down a bit and talk about redb.Core — the data engine at the heart of the RedBase ecosystem. The other pieces (redb.Route for pipelines, redb.Tsak for cluster runtime) lean on it, but this post is just about the database part.

I’ve been working on this project for several years. It started as an attempt to get rid of migrations and turned into what it is now — a typed object store for .NET over PostgreSQL and MSSQL.

It’s not a weekend prototype. The free packages on NuGet are at version 2.0, there are 43 packages across the ecosystem, the architecture went through three rewrites, and as of this week it’s been running 3 months on production at a 30-year national food distributor (more on that below).

This post is a technical walkthrough of redb.Core — what it is, how it differs from EF Core, what the generated SQL actually looks like, what the production workload looks like, and what’s shipping next.

What redb.Core actually is

github.com/redbase-app/redb — Apache 2.0

RedBase stores typed C# objects in two tables (_objects + _values) over PostgreSQL or Microsoft SQL Server. Not JSON blobs. Not JSONB. Real typed columns with FK constraints — NUMERIC(38,18) for money, timestamptz for dates, uuid for GUIDs. Real B-tree indexes. ACID transactions.

The schema is your C# class:

[RedbScheme("Employee")]
public class EmployeeProps
{
    public string FirstName   { get; set; } = "";
    public string LastName    { get; set; } = "";
    public int    Age         { get; set; }
    public decimal Salary     { get; set; }
    public DateTime HireDate  { get; set; }
    public string[]? Skills   { get; set; }
    public Address? HomeAddress { get; set; }
    public Dictionary<int, decimal>? BonusByYear { get; set; }
}

That attribute is the entire schema definition. Call SyncSchemeAsync<EmployeeProps>() once — done. Add a property next sprint — redeploy, call sync again. Old objects still work. No migration files. No DBA ticket. No 2am rollback story.

// Save — entire object graph, one call
await redb.SaveAsync(employee);

// Load — full graph, arrays, dicts, nested classes — all materialized
var e = await redb.LoadAsync<EmployeeProps>(id);

// Query — real LINQ, real SQL
var seniors = await redb.Query<EmployeeProps>()
    .Where(e => e.Salary > 100_000 && e.Age >= 35)
    .OrderByDescending(e => e.Salary)
    .Take(50)
    .ToListAsync();

No DbContext. No Include chains. No Add-Migration. No mapper layer.

Object graphs in one call

This is the part that surprises people coming from EF. Props can contain other Props — single references, arrays, dictionaries — and the entire graph saves and loads as one operation:

[RedbScheme("Order")]
public class OrderProps
{
    public Customer Customer { get; set; }                          // nested class
    public Address ShippingAddress { get; set; }                    // nested class
    public Product[] Products { get; set; }                         // array of classes
    public RedbObject<PaymentProps>[] Payments { get; set; }        // array of full objects with own IDs
    public Dictionary<string, RedbObject<CouponProps>> Coupons { get; set; }  // dict of objects
    public Dictionary<(int Year, string Quarter), string> Reviews { get; set; } // tuple-key dict
}

await redb.SaveAsync(order);   // entire graph persisted, FK ordering handled
var loaded = await redb.LoadAsync<OrderProps>(id);
// loaded.Props.Customer.Address — ready
// loaded.Props.Payments[0].Props — ready (full RedbObject with own Id, DateCreate, etc.)
// loaded.Props.Coupons["SUMMER20"].Props — ready

In EF Core this would be 28 tables, ~40 Include/ThenInclude calls, manual junction tables for the many-to-many, and INSERT ordering that breaks every time someone adds a non-nullable FK without a default.

In RedBase: one SaveAsync, one LoadAsync. The nested RedbObject instances are real first-class objects — they have their own IDs, their own timestamps, they can be queried independently, they participate in tree structures. They are not denormalised JSON glued to the parent.

What the LINQ actually compiles to

Where(e => e.Salary > 100_000 && e.Age >= 35) doesn’t get serialized to JSON and re-parsed (that’s the Free engine’s path — covered later). In Pro, the C# expression tree is walked node-by-node and emitted as parameterized SQL. Roughly:

WITH pvt AS (
  SELECT v._id_object,
         (array_agg(v._Numeric) FILTER (WHERE v._id_structure = $1))[1] AS "Salary",
         (array_agg(v._Long)    FILTER (WHERE v._id_structure = $2))[1] AS "Age"
    FROM _values v
   WHERE v._id_structure = ANY($3::bigint[])
     AND v._id_object IN (SELECT o._id FROM _objects o WHERE o._id_scheme = $4)
   GROUP BY v._id_object
)
SELECT o.*
  FROM _objects o
  JOIN pvt ON pvt._id_object = o._id
 WHERE pvt."Salary" > $5
   AND pvt."Age"    >= $6
 ORDER BY pvt."Salary" DESC
 LIMIT 50;

Parameterized. Plan-cached by PostgreSQL. One index scan on (_id_structure, _id_object), one aggregation pass, B-tree filter on flat columns. The number of filter fields doesn’t change the shape of the query.

The C# → SQL compiler handles arithmetic (*, +, %), Math.*, String.Contains/StartsWith/Trim/ToLower, DateTime.Year/Month/..., nullable navigation (x.Address?.City) compiled to IS NOT NULL, the ternary operator compiled to CASE WHEN, StringComparison.OrdinalIgnoreCase compiled to native ILIKE, dictionary access dict["key"] compiled to pivot columns, and a few more edge cases that EF Core itself doesn’t always handle.

You can preview the SQL of any query without executing it:

var sql = await query.ToSqlStringAsync();

Like IQueryable.ToQueryString() in EF, but works for trees, GroupBy, window functions too.

Why bulk save is so fast — two tables, two streams

Something to understand before the change-tracking section: the storage layout is two tables, so SaveAsync of a batch is two bulk operations, not N round-trips.

On PostgreSQL the provider uses Npgsql’s BeginBinaryImportAsync — native COPY protocol, binary format:

// from redb.Postgres/Data/NpgsqlBulkOperations.cs
await using var writer = await conn.BeginBinaryImportAsync(
    "COPY _objects (_id, _id_parent, _id_scheme, _name, ...) FROM STDIN (FORMAT BINARY)");
foreach (var obj in objectsList) {
    await writer.StartRowAsync();
    await writer.WriteAsync(obj.Id,        NpgsqlDbType.Bigint);
    await writer.WriteAsync(obj.IdScheme,  NpgsqlDbType.Bigint);
    // ... typed writes
}
await writer.CompleteAsync();

For a batch save: one COPY stream writes the _objects rows, another writes the _values rows. Two streams, no per-row round-trips, no string-formatted INSERTs. MSSQL uses SqlBulkCopy for the same role.

This is why 1000 routes × ~40 fields = ~40,000 value rows save in tens of milliseconds inside the 200–300 ms budget. The bottleneck is the network round-trip and the COPY write, not the ORM machinery — there isn’t any ORM machinery in the hot path.

Change tracking without DbContext (Pro)

DbContext keeps an in-memory snapshot of every entity you load — that’s how it knows what changed. It’s also why it isn’t thread-safe and why the cache dies with the request.

RedBase Pro takes a different approach. With PropsSaveStrategy.ChangeTracking (Pro only; the free tier uses DeleteInsert), SaveAsync does this on a batch:

  1. One bulk SELECT of existing values for all object IDs being saved.
  2. Build two ValueTreeNode trees — one from your in-memory objects, one from the DB state.
  3. Structural diff — subtrees with matching hashes are skipped entirely (no value comparison, no child traversal). Inserts, updates, and deletes are computed per node.
  4. Three bulk operations — BulkInsertValuesAsync, BulkUpdateValuesAsync, BulkDeleteValuesAsync — each a single round-trip. Inserts go through COPY BINARY again.

Net effect: changing one field in a deeply nested object emits one UPDATE, not a full delete-and-reinsert of the entire props graph. And the comparison happens in C# on the application side — no DbContext lifetime to worry about, safe to run from a background Channel consumer or a Parallel.ForEach.

(In the production code from the previous section, the application also does its own obj.ComputeHash() check at the route level. That part is optional business-code — you could call SaveAsync on every route and the Pro tree-diff would still skip unchanged values internally. It’s there as a coarse pre-filter so unchanged routes don’t even enter the save pipeline at all, saving the diff work too.)

Production deployment — the numbers

The biggest deployment right now:

  • 30-year national HoReCa food distributor
  • ~150,000 orders/month, ~20,000 B2B customers, 600+ cities
  • 3-node cluster, 4 cores / 8 GB RAM / 50 GB SSD per node
  • ~550 daily internal users (operators, drivers, supervisors, dispatch, back-office)
  • 10–15% CPU under full load
  • Integrations: SAP, Kafka, RabbitMQ, GPS feeds, Mercury / EGAIS / government APIs

Three months in production, no data-layer incidents. Two projects in the company use it, the second one came after the first one proved stable.

Real workload, real timings

The hottest pipeline is the SAP monitoring sync. Every 60 seconds a SQL polling consumer calls a stored procedure on SAP S/4 (usp_TsUM_MonitoringReport_xml), gets ~1000 transportation orders back as XML, syncs reference dictionaries (drivers, vehicles, list items), bulk-loads existing routes from RedBase, hash-compares each one, and saves only the changed ones.

The whole loop fits in ~200–300 ms for ~1000 routes. The actual production code:

From("direct://tsum")
    .RouteId("tsum-processing")
    .ProcessWithRedb(async (redb, exchange, ct) =>
    {
        var orders = (List<TransportationOrder>)exchange.In.Body;

        var sw = Stopwatch.StartNew();

        // 1. Sync dictionaries (Drivers, Vehicles, ListItems) — only new/changed
        var dicts = await DictionarySyncService.SyncFromOrdersAsync(redb, orders, ct);
        if (dicts.DriversNew + dicts.DriversChanged + dicts.VehiclesNew
            + dicts.VehiclesChanged + dicts.ListItemsRelinked > 0)
            await RefDataCache.RefreshAsync(redb);
        var syncMs = sw.ElapsedMilliseconds;

        // 2. Bulk load existing routes by Code — one query, ~1000 codes
        var codes = orders.Select(o => o.Code).ToList();
        var existing = await redb.Query<TransportationRoute>()
            .WhereRedb(o => codes.Contains(o.ValueString!))
            .ToListAsync();
        var existingByCode = existing.ToDictionary(o => o.ValueString!, o => o);

        // 3. Merge: update if hash changed, insert if new, skip if unchanged
        var toSave = new List<IRedbObject>();
        var updatedCount = 0;
        var skippedCount = 0;
        foreach (var order in orders)
        {
            var routeProps = MapOrderToRouteProps(order, dicts);
            if (existingByCode.TryGetValue(order.Code, out var obj))
            {
                var hashBefore = obj.ComputeHash();
                EnrichRouteFromOrder(obj.Props!, routeProps);
                if (obj.ComputeHash() != hashBefore) { toSave.Add(obj); updatedCount++; }
                else skippedCount++;
            }
            else
            {
                toSave.Add(new RedbObject<TransportationRoute>
                {
                    name = $"Route {order.Code}",
                    value_string = order.Code,
                    Props = routeProps
                });
            }
        }

        // 4. One batched save — mixed inserts + updates
        if (toSave.Count > 0)
            await redb.SaveAsync(toSave);

        Logger.LogInformation(
            "[TSUM] orders={Orders} routes(+{Created} ~{Updated} ={Skipped}) " +
            "drivers(+{DN} ~{DC}) vehicles(+{VN} ~{VC}) " +
            "sync={Sync} query={Query} save={Save} total={Total}ms",
            orders.Count, toSave.Count - updatedCount, updatedCount, skippedCount,
            dicts.DriversNew, dicts.DriversChanged,
            dicts.VehiclesNew, dicts.VehiclesChanged,
            syncMs, queryMs, saveMs, sw.ElapsedMilliseconds);
    });

TransportationRoute has ~40 fields and 12 RedbListItem references (Driver, Vehicle, CarMark, ShippingPoint, BusinessType, PlaceTo, PlaceFrom, LoadingZone, TransportStatus, Risk, DeliveryStatus, LoadStatus) plus 2 object references to AD users. Every single one is a foreign key in the database. None of them require a JOIN at query time — the materializer handles it.

Smaller queries (point-lookup of a single route, dictionary fetch, REST endpoints for the UI) run in 50–100 ms including HTTP overhead.

What’s actually in the database

After running this in production, the storage looks like this:

  • ~1000 transportation routes/day, plus delivery points, transport snapshots, garage states, slice settings, slice snapshots, drivers, vehicles, yard places, AD user refs — about a dozen [RedbScheme] classes in active use
  • ~500,000 objects in _objects (routes, points, snapshots, dictionary items)
  • ~15,000,000 rows in _values (every typed property of every object)
  • All of that lives in 2 tables — _objects and _values — plus the system tables for schemes, structures, lists, users, permissions

15 million typed value rows. Two tables. No 30-table schema. No 200 migration files. The query times above are on this dataset.

If you tried to model the same domain in EF Core flat tables, you’d end up with roughly: Routes, RoutePoints, TransportSnapshots, GarageStates, SliceSettings, SliceSnapshots, Drivers, Vehicles, CarMarks, ShippingPoints, BusinessTypes, YardPlaces, LoadingZones, TransportStatuses, DeliveryStatuses, Risks, TripRisks, AdUsers — plus junction tables and audit tables for each. 30+ tables, dozens of migrations, and every schema change is a deploy event.

In RedBase the schema change is git push. The next InitializeAsync() call adds the new structure rows. Done.

What would EF Core look like here?

I asked myself the same question before starting. Let’s count what EF would need:

  • TransportationRoute entity → 1 table
  • 12 ListItem references → 12 lookup tables + 12 nullable FKs
  • 2 AD user references → 2 more FKs
  • The 1000-route batch update → 1000 entities tracked in DbContext for change detection
  • Hash-based skip of unchanged objects → doesn’t exist in EF out of the box; you write it manually
  • The same logic across multiple processes/routes → DbContext is per-request, the change-tracker cache dies at the end of every batch

The realistic EF flow looks like this:

// Load existing — needs Include for every lookup or you get N+1
var existing = await db.Routes
    .Include(r => r.Driver)
    .Include(r => r.Vehicle)
    .Include(r => r.CarMark)
    .Include(r => r.ShippingPoint)
    .Include(r => r.BusinessType)
    .Include(r => r.PlaceTo)
    .Include(r => r.PlaceFrom)
    .Include(r => r.LoadingZone)
    .Include(r => r.TransportStatus)
    .Include(r => r.Risk)
    .Include(r => r.DeliveryStatus)
    .Include(r => r.LoadStatus)
    .Include(r => r.RiskSetBy)
    .Include(r => r.KcResponsible)
    .Where(r => codes.Contains(r.Code))
    .ToListAsync();

// Map, mutate, SaveChanges — 1000 tracked entities, full diff snapshot in RAM
// SaveChanges fires N UPDATE statements (one per row) in a transaction
await db.SaveChangesAsync();

In practice on a workload like this:

  • The Include chain on 14 lookups produces a query that PostgreSQL/MSSQL can’t always optimise cleanly (cartesian explosion risk; you split-query and pay multiple round trips).
  • SaveChanges on 1000 modified entities emits 1000 individual UPDATEs unless you reach for EFCore.BulkExtensions or similar third-party libraries. RedBase ships COPY BINARY for inserts and a batched UPDATE path for updates in the box.
  • The DbContext snapshot of 1000 tracked entities, each with 14 included lookups, is real memory pressure. AsNoTracking() is faster but you lose change detection — you have to re-implement it.
  • The 200–300 ms budget on a 4-core container is not realistic in this scenario without significant manual optimisation and probably a separate bulk-update path.

I’m not saying EF can’t do it. I’m saying the equivalent EF implementation is more code, more moving parts, and significantly slower without third-party packages. RedBase ships a single SaveAsync(toSave) and a hash-based skip primitive in the box.

The hash comparison itself (obj.ComputeHash() != hashBefore) is application-level business code — it’s optional; the Pro tree-diff inside SaveAsync would skip unchanged values anyway. But used like this it lets the application skip the ~95% of routes that didn’t change between SAP polls before they even enter the save pipeline. Combined with COPY BINARY into two tables and the Pro tree-diff for the routes that do change, the whole loop fits the 200–300 ms budget. No equivalent set of built-in primitives exists in EF Core; you either snapshot manually, re-save everything, or pull in third-party bulk packages.

What EF Core does with complex objects (vs what RedBase does)

The E000 benchmark uses EmployeeProps — a realistic model with nested classes, arrays, dictionaries, and RedbObject references:

What classic ORM needs RedBase
~28 tables 2 tables
FK ordering on every INSERT Single SaveAsync
40+ Include/ThenInclude calls LoadAsync<T>(id) — one line
Migration file for every new field Add property, call SyncSchemeAsync, done
~5,000 INSERTs for 100 employees 1 BulkInsert via COPY protocol

For 100 complex employees: EF Core ≈ 4,000–6,000 INSERTs across 28 tables in FK order. RedBase: ~3,000 typed value rows, one COPY command.

And the test results are published — 525 automated tests across all editions and both databases. All green.

The query engine: free vs Pro

This is where it gets technically interesting.

Free edition compiles your LINQ lambda to a JSON facet format, then calls a plpgsql stored procedure that parses that JSON and generates SQL dynamically. For simple 1–2 field filters, PostgreSQL optimizes correlated EXISTS subqueries well. It works.

Pro edition walks the C# expression tree directly in C# (ExpressionToSqlCompiler), emits native parameterized SQL. Same plan every call — PostgreSQL can cache it. 3–10× faster on complex multi-field filters.

But here’s what’s happening right now: I’m porting the Pro PVT CTE query engine into the free tier. The plan is full parity on PVT — same CTE shape, same expression coverage, same projection capabilities. Free and Pro will speak the same query dialect.

The remaining differences between Free and Pro will be elsewhere:

  • Query plan caching. Pro builds parameterized SQL in C# (ExpressionToSqlCompiler) — same shape every call, PostgreSQL caches the plan once and reuses it. The free tier generates SQL inside plpgsql with literal values inlined (properly escaped to prevent injection), so each call can produce a slightly different plan. Same correctness, different plan-cache behavior.
  • Materialization. Pro has a parallel materializer (multiple values streams hydrated concurrently into the object graph). Free uses a simpler sequential path.
  • Save strategy. PropsSaveStrategy.ChangeTracking (tree-diff with hash-based subtree skip) is Pro-only. Free uses DeleteInsert — still bulk via COPY, but no per-field diff.

So Pro stays faster on hot paths and large object graphs, but the query language itself — what you can write in .Where, .Select, projections, grouping, having, FTS, regex — will be the same on both sides.

Show me the SQL it generates

Here’s a real example — a projection query on EmployeeProps with 16 computed columns:
full names, salary calculations, date extractions, seniority classification via CASE, coalesce for nullable fields, explicit type cast. This is the free tier:

WITH _pvt_cte AS (
    SELECT
        v._id_object,
        (array_agg(v._Long)    FILTER (WHERE v._id_structure = 1000017))[1] AS "Age",
        (array_agg(v._Numeric) FILTER (WHERE v._id_structure = 1000020))[1] AS "Salary",
        (array_agg(v._DateTimeOffset) FILTER (WHERE v._id_structure = 1000018))[1] AS "HireDate",
        (array_agg(v._String)  FILTER (WHERE v._id_structure = 1000016))[1] AS "LastName",
        (array_agg(v._String)  FILTER (WHERE v._id_structure = 1000015))[1] AS "FirstName"
    FROM _values v
    WHERE v._id_structure = ANY(ARRAY[1000015,1000016,1000017,1000018,1000020]::bigint[])
      AND v._id_object IN (SELECT o._id FROM _objects o WHERE o._id_scheme = 1000014)
    GROUP BY v._id_object
)
SELECT
    o._id                                                          AS id,
    ("FirstName" || ' ' || "LastName")                            AS full_name,
    UPPER("FirstName")                                             AS upper_name,
    LENGTH("FirstName")                                            AS name_len,
    ("Salary" * 12)                                                AS yearly_x12,
    (("Salary" * 0.15) - ("Salary" * 0.013))                      AS bonus_after_tax,
    ("Salary" / ("Age" + 1))                                       AS avg_per_year,
    ("Age" % 10)                                                    AS age_mod10,
    ABS(("Age" - 35))                                              AS abs_diff,
    FLOOR(("Salary" / 2))                                          AS floor_half_salary,
    EXTRACT(YEAR  FROM "HireDate")                                 AS hire_year,
    EXTRACT(MONTH FROM "HireDate")                                 AS hire_month,
    EXTRACT(YEAR  FROM AGE("HireDate", '2026-05-20'))              AS tenure_years,
    CASE
        WHEN ("Age" >= 60) THEN 'senior'
        WHEN ("Age" >= 35) THEN 'mid'
        ELSE 'junior'
    END                                                            AS seniority,
    COALESCE("FirstName", '<unknown>')                             AS display_name,
    ("Age")::text                                                  AS age_as_text
FROM _pvt_cte
JOIN _objects o ON o._id = _pvt_cte._id_object
WHERE "Salary" > '0'::numeric
ORDER BY "Salary" DESC
LIMIT 3

One _values scan. One GROUP BY. Sixteen computed output columns. Standard SQL — no magic, fully readable in pgAdmin, fully EXPLAIN ANALYZE-able.

This query was generated by pvt_build_projection_sql() — a plpgsql function that is shipping in the next free release.

When this fits and when it doesn’t

Honest take.

Fits well: business apps with collections, nested structures, hierarchies, dictionaries, or schemas that change often. Apps where a business analyst can ask “add a field” and you want to ship the same day. Anything where you’d otherwise end up with 28 tables for one logical entity.

Doesn’t fit: flat reporting databases with five fixed tables. Use Dapper there — it’s honest and fast.

The argument I’d push back on is the common one — “for 80% of projects EF Core is enough”. In my experience, the moment you have collections inside an entity, lookup data, dictionaries, tree structures, or polymorphic types, the migration tax compounds fast. RedBase removes that whole class of work. For the remaining 20% of cases — pure flat schemas — it’s overkill, agreed.

And two more axes that usually get left out of that comparison:

  • Extensibility. Adding a field is a property in C# and one InitializeAsync() call — no migration script, no review, no deploy window. The same change in an EF stack is at minimum: model edit + Add-Migration + reviewed SQL + coordinated deploy. Multiply by every iteration in a year.
  • Cost of development. On the TsUM project the data-layer work — schemas, storage, queries, sync pipelines, change-tracking, bulk save — came in at roughly 128 person-hours. The estimate for the same scope on a classic EF + handwritten bulk-update + ASP.NET Identity + 30-table migration stack was on the order of 3000 person-hours. That’s not a typo and it’s not marketing — it’s the gap between “add a property, redeploy” and “add a column, write migration, update DTOs, update mapper, update repository, write tests for all of it”. The 80%/20% framing only holds if you count lines of code; once you count engineer-hours over a project lifetime it inverts.

What else is in the box

195+ working examples in the repo. The shortlist of things people usually don’t expect to be built-in:

// Tree queries — recursive CTE underneath, no CTE to write yourself
var products = await redb.TreeQuery<Product>(londonHQ.Id, maxDepth: 10)
    .Where(p => p.InStock)
    .ToListAsync();

// Soft delete with atomic mark + background purge + progress in DB
await redb.SoftDeleteAsync(ids, user);

Also shipping in the core (this is the short list — the real list is much longer):

  • GroupBy + window functions in one query
  • Built-in users / roles / permissions (no ASP.NET Identity dependency, no separate auth schema)
  • Export / import via .redb files (PostgreSQL ↔ MSSQL portability)
  • Multi-database in one process with full domain isolation — separate connections, separate scheme caches, separate object caches per domain; one app can talk to several independent RedBase databases at the same time without their metadata leaking into each other
  • Scheme cache and object cache (both per-domain), invalidated on SyncSchemeAsync / SaveAsync, used by the materializer to skip repeated metadata lookups
  • Atomic soft-delete with background purge and progress visible in the DB
  • Tree queries with recursive CTE
  • Polymorphic trees — different C# types at each level of the same tree

There’s a lot more that doesn’t fit in one section. The architecture page has the full map.

What’s coming next

The FreePvtQuery release is the nearest milestone. This is a complete rewrite of the free tier’s query engine based on the PVT CTE architecture — bringing it to full parity with Pro on the query side:

  • Single-pass _values scan (vs correlated EXISTS per field)
  • Full expression system: arithmetic, string, date, math, regex, FTS, CASE, coalesce, cast
  • Projections with arbitrary computed columns (like the SQL above)
  • HAVING, DISTINCT ON, GROUP BY extensions

After that: MSSQL support for the new engine, C# LINQ-to-JSON bridge so you keep writing .Where(e => e.Salary > 100_000) without touching JSON. Pro will keep its edge on plan caching (parameterized SQL), parallel materialization, and tree-diff change tracking.

This is maybe 10% of what’s in there

This post covered the basics: storage model, query engine, free PVT demo, production deployment. Topics I didn’t touch:

  • Polymorphic tree hierarchies (different C# types at every level, same tree API)
  • Object graphs with RedbObject<T> references inside Props
  • Data migrations (Pro): fluent API, computed columns, in-place type changes
  • Export/import: .redb files (JSONL/ZIP), PostgreSQL ↔ MSSQL portability
  • Multi-database in one process (domain-isolated caches)
  • redb.CLI — schema management from command line
  • redb.PropsEditor — runtime props editing UI
  • Integration with redb.Route (22-transport pipeline engine) — separate article

If there’s a direction you want covered next — migrations, polymorphic trees, benchmarks, internals of the diff-tree change tracking — drop it in the comments and that’s what the next post will be.

Links

There’s a lot of material. If you want to go deep without reading three articles, paste these into your AI of choice and ask it to analyse the architecture:

Questions in the comments — I’ll answer them directly.

Join the GitHub Finish-Up-A-Thon Challenge: $3,000 Prize Pool!

We’re thrilled to announce our newest challenge with GitHub!

Running through June 7, the GitHub Finish-Up-A-Thon Challenge invites you to finally give that abandoned side project, challenge submission, or hackathon creation the love it deserves. GitHub is looking for ways to empower builders to finish what they started, and this challenge is the first step.

There’s a $3,000 prize pool on the line for those of you who are up to the challenge. Ten winners will be selected and will also receive a DEV++ membership along with an exclusive winner badge.

Read on to learn more.

Our Prompt

Your mandate is to revive and finish a project you started but never completed.

You built something cool under pressure, likely with too little sleep and not enough time. Now it’s your chance to pick it back up, add some polish, and share it with the world.

The most important aspect? Show us your before and after, and how GitHub Copilot helped you get there.

Judging Criteria

All qualified entries will be judged based on the following criteria:

  • Use of underlying technology
  • Usability and User Experience
  • Originality and Creativity
  • Completion Arc: Is there a clear before and after journey?

Prizes

Ten Winners will each receive:

  • $300 USD
  • DEV++ Membership
  • Exclusive DEV Badge

All Participants with a valid submission will receive a completion badge on their DEV profile.

How To Participate

In order to participate, share your revived project and publish a post using the submission template below.

GitHub Finish-Up-A-Thon Challenge Submission Template

Please review our judging criteria, rules, guidelines, and FAQ page before submitting so you understand our participation guidelines and official contest rules such as eligibility requirements.

Important Dates

  • May 21: GitHub Finish-Up-A-Thon Challenge begins!
  • June 7: Submissions due at 11:59 PM PST
  • June 18: Winners Announced

We can’t wait to see what you finally ship! Questions about the challenge? Ask them below.

Good luck and happy building!

I Replaced a $50/Month OCR API with Gemma 4’s Native Vision (And You Can Too)

These optional micro-tweaks provide the perfect edge. Refining the model nomenclature to match the official Gemma 4 E4B (4B) release conventions, embedding a hardware baseline disclaimer for non-GPU laptops, and throwing a real-world analytics example into the chart-parsing matrix layer elevates this into absolute top-tier production reference material.

Following the layout rules for artifact compilation, the vision pipeline architecture has been represented as a clean, text-based workflow vector directly inside the content stream.

Everyone is talking about Gemma 4’s 128K context window. But the real sleeper architectural feature is its native client-side vision—and it just saved my side project $50 a month.

When Google announced Gemma 4, the developer world fixated on the massive context window, the Mixture‑of‑Experts efficiency metrics, and the flexible Apache 2.0 license. All of that praise is completely deserved.

But almost no one is talking about the native multimodal input engine—the structural ability to feed the model images directly without a separate, fragile OCR pipeline or third-party captioning tool.

I run a small local automation that extracts line items from messy, scanned contractor invoices. Until last week, I was paying a premium for a cloud-based OCR API that turned raw JPEGs into digital text. Then I tried deploying the Gemma 4 E4B (4B) variant completely locally on my laptop.

It worked. Perfectly. And it cost me absolutely nothing but a fraction of local electricity.

This is the story of how Gemma 4’s vision capabilities can replace expensive, closed cloud services, what the actual performance trade-offs are, and exactly how to implement the processing pipelines yourself.

The Real-World Friction: Scanned Documents Have No Text Layers

Most “smart” data extraction tutorials assume your source data is pristine. A clean CSV, a well-formatted HTML layout, or a digital PDF with crisp selectable text.

Real-world operations look completely different. Invoices, receipts, and field contracts are often scanned photographs—creased, shadowed, skewed, or hand-filled with a pen. My contractor regularly sends me mobile phone photos of hand-filled invoices. The cloud OCR service I used previously handled them decently, but it carried significant friction:

  • Financial Overhead: $0.10 per page $times$ 500 pages/month = $50/month flat cost.
  • Network Latency: The external API round-trip took 3–5 seconds per image payload.
  • Privacy Exposure: Sensitive accounting vectors and customer identifiers had to leave my machine on every single execution run.

I needed a local, private, and zero-marginal-cost alternative. Gemma 4’s vision framework turned out to be exactly that.

What Gemma 4’s Native Vision Actually Does

Gemma 4 natively processes image inputs down at the weight level. You do not need a separate visual wrapper, an image captioning pre-model, or a legacy local Tesseract binary installation on your environment. You simply pass the raw image bytes directly into the interface wrapper, and the model reasons over the visual content directly.

Under the hood, Gemma 4 passes your visual file array through a specialized native layer that projects pixel patches directly into the exact same high-dimensional embedding space used by standard text tokens.

[ Raw Image Ingestion ] ➔ [ 2D Patch Segmentation ] ➔ [ Vision Encoder Passes ] ➔ [ Shared Token Embedding Space ] ➔ [ Unified Text Decoders ]

Because it bypasses intermediate text conversion, the model can natively execute deep cross-modal reasoning over spatial vectors:

  • Reading distorted, skewed text from raw photographs.
  • Discerning complex structural layout hierarchies like tables, column boundaries, checkboxes, and signature lines.
  • Describing and extracting trends from analytical diagrams, charts, and technical wireframes.

For my specific pipeline requirements—extracting three core fields from an invoice photo—the lightweight Gemma 4 E4B model was remarkably capable.

Step‑by‑Step: Moving Your OCR Pipeline Local

I run this setup on a standard MacBook Pro (M1, 16GB RAM) utilizing Ollama as the local model runtime engine.

1. Pull the Multimodal Footprint

Ollama handles Gemma 4’s vision variants natively out of the box. The lightweight E4B architecture runs comfortably inside less than 8GB of active memory overhead:

ollama pull gemma4:e4b

2. Implement the Unified Python Extraction Script

We use the official python ollama SDK. The API allows us to pass local file locations or raw base64 data streams cleanly inside the unified message structure:

import ollama

def extract_invoice_fields(image_path):
    response = ollama.chat(
        model='gemma4:e4b',
        messages=[{
            'role': 'user',
            'content': '''You are a strict data extraction engine. Analyze this document image and return ONLY a single valid JSON object matching this exact schema:
{
  "date": "YYYY-MM-DD",
  "amount": number_no_symbols,
  "description": "concise_summary"
}
If a field is completely unidentifiable, set its value to null. Do not include markdown formatting wraps, conversational intros, or post-explanations.''',
            'images': [image_path]
        }],
        options={'temperature': 0.1}
    )
    return response['message']['content']

# Execution Run
result = extract_invoice_fields('contractor_invoice.jpg')
print(result)

Hardware Performance Note: On my M1 MacBook Pro (16GB RAM), inference takes ~2.3 seconds per image using default GPU metal acceleration pathways. CPU-only architectures or older workstations may see latencies scale up to 4-6 seconds, but the underlying pipeline remains entirely stable and functional.

📊 Real-World Processing Output

When fed a shadowy, tilted smartphone photo of an invoice line item, the local model evaluates the matrix and returns clean, structured JSON data directly to the terminal:

{
  "date": "2026-05-15",
  "amount": 1240.50,
  "description": "Electrical panel upgrade"
}

Production Benchmarks: Local Vision vs. Cloud OCR

To evaluate the feasibility of removing our paid cloud layer, I ran a comparative benchmark test over a batch of 50 real contractor invoice photographs featuring heavy shadows, uneven contrast, folds, and handwriting.

Performance Metric Closed-Source Cloud OCR API Local Gemma 4 E4B Runtime
Field Accuracy (Exact Match) 94% 91%
Average Pipeline Latency 4.2 seconds (Network Bound) 2.3 seconds (Local Hardware)
Operational Cost (Per 1k Pages) $100.00 $0.00 (Negligible Electricity Only)
Data Privacy Guardrail Leaves local machine architecture 100% Local Sandboxed Footprint
Offline Operational Capability No Yes

While the cloud API held a slight 3% advantage on edge-case handwriting styles due to proprietary training set scale, Gemma 4 dominated on speed, security, and cost.

💡 Elevating Local Accuracy via OpenCV Preprocessing

You can easily close that 3% accuracy deficit by applying basic computer-vision filters to clean up the image before the model infers the token paths. By deploying this simple two-line graying and adaptive contrast filter using opencv-python, local extraction accuracy jumped up to 95%:

import cv2

def preprocess_document_image(image_path, target_output_path):
    # Load image, convert to grayscale, and apply adaptive histogram equalization
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
    enhanced_contrast = clahe.apply(gray)

    cv2.imwrite(target_output_path, enhanced_contrast)

Operational Trade-Off Matrix: When Local Vision Works

Multimodal open-weight models are powerful, but they are not magic. Here is a definitive assessment of where local vision excels versus where it faces challenges:

Visual Document Target Capability Level Engineering Notes
Clean Machine-Printed Text ✅ Excellent Matches OCR accuracy with cleaner formatting preservation.
Distinct Handwritten Numbers ✅ Good Exceeds 90% accuracy if numerical alignment is distinct.
Cursive or Connected Prose ⚠️ Mixed Prints parse easily; rapid cursive occasionally drops characters.
Complex Interleaved Tables ✅ Very Good Excels at keeping row-to-column context alignment intact.
Low-Light / Blurry Inputs ❌ Degrades Fast Requires active contrast preprocessing to prevent hallucinations.
Multi-Axis Charts & Graphs ✅ Excellent Can synthesize and describe visual trends natively (e.g., ‘explain why Q3 revenue dipped’).
Barcodes & Matrix QR Codes ❌ Incompatible Do not use LLMs for this; leverage dedicated lightweight libraries.

The Broader Paradigm Shift: Beyond Simple Text Extraction

Once you realize that an open-weight model running locally can natively see, your pipeline horizons expand past basic invoice automation. You can implement this exact same sandboxed visual loop to drive advanced engineering workflows completely offline:

  • Automated Accessibility Auditing: Pipe live web application UI screenshots into Gemma 4 and prompt it to flag contrast violations, broken text crops, or missing aria structural targets.
  • Visual Error Diagnosis: Programmatically capture app crash states or native CLI core dumps, pass the screenshot directly to the model, and allow it to read the visual stack trace to suggest a codebase patch.
  • Mockup-to-Component Suggestions: Feed a raw mockup screenshot of a specific frontend asset—like a three-column pricing table or a complex registration form—directly into your local model and ask: “Write clean, responsive Tailwind CSS code to replicate this exact visual layout structure.” It provides an instant starter component blueprint without leaving your secure workspace.

The Bottom Line

Gemma 4’s multimodal capability wasn’t the loudest headline of the launch cycle, but it represents a massive workflow victory for independent software developers.

By replacing a cloud-dependent service with a sandboxed 4B parameter architecture, my pipeline runs faster, preserves complete data privacy, and cuts my API billing cycle down to zero. If you are still managing brittle Tesseract configurations or paying regular subscription invoices for proprietary OCR pipelines, pull down Gemma 4’s vision weights and start testing locally today.

🔗 Resources & Tooling

💬 Let’s Talk Local Document Processing

Are you currently relying on cloud API endpoints to manage document ingestion and semantic image parsing for your software apps, or have you started moving these pipelines down to local edge weights?

Drop your processing speeds, hardware benchmarks, and preprocessing strategies in the comments below—let’s build a clean blueprint for local-first visual automation!

🤖 AI Transparency Disclosure

In full compliance with the challenge transparency criteria:

  • Writing Assistance: I utilized an AI companion (Gemini) to restructure raw benchmark blocks into clean markdown tables, format unified parameter keys within code wrappers, and balance prose scannability. All core pipeline metrics, benchmarking logic, and design viewpoints are completely my own.
  • Visual Assets: The split-screen verification cover image was generated using Gemini.
  • Originality Verification: The software integration scripts, local open-weight runtime benchmarking passes, and image filter pipelines were implemented and executed entirely on my local development hardware.

Building a Data-Driven Medical Image Enhancement Pipeline with Differential Evolution 🔥🩻

How I Built an AI-Driven Medical X-Ray Enhancement Framework Using Differential Evolution

Medical images are not just images.

Sometimes they contain:

  • an early disease sign
  • a hidden fracture
  • a tiny abnormality
  • or the difference between a clear diagnosis and uncertainty

But in many real-world X-ray images, important details already exist inside the image — they are simply difficult to visualize.

Low contrast, poor illumination, sensor limitations, and acquisition conditions can hide critical anatomical structures and fine textures.

This project started with a simple question:

Can we automatically enhance difficult medical X-ray images without destroying their structural integrity?

The answer led me into a fascinating combination of:

  • image processing
  • evolutionary optimization
  • adaptive contrast enhancement
  • and medical imaging research

In this article, I’ll walk through how I built an AI-driven medical image enhancement framework using Differential Evolution and Piecewise Linear Transformation.

The Problem with Traditional Enhancement Methods

Most classical enhancement methods focus mainly on brightness redistribution.

Techniques like:

  • Histogram Equalization (HE)
  • CLAHE
  • Linear Contrast Stretching

can improve global contrast, but they often introduce new problems:

  • over-enhancement
  • noise amplification
  • loss of local structures
  • unnatural brightness
  • washed-out regions

This becomes especially problematic in medical imaging.

Medical images require a delicate balance between:

  • detail visibility
  • structural preservation
  • natural appearance
  • and diagnostic reliability

Simply making the image brighter is not enough.

The Main Idea Behind the Framework

Instead of applying one fixed enhancement transformation to the entire image, I designed a framework that adapts itself automatically to each image.

The pipeline combines:

  1. Adaptive Piecewise Linear Transformation (PLT)
  2. Multi-Otsu initialization
  3. Differential Evolution optimization
  4. Gamma refinement
  5. Edge-aware sharpening

The goal was to create an enhancement system capable of:

  • revealing hidden details
  • improving local contrast
  • preserving structural fidelity
  • and maintaining realistic medical appearance

The Enhancement Pipeline

The full enhancement workflow looks like this:

Low-Contrast X-Ray
        ↓
 Image Normalization
        ↓
 Multi-Otsu Segmentation
        ↓
 Piecewise Linear Transformation
        ↓
 Differential Evolution Optimization
        ↓
 Gamma Refinement
        ↓
 Edge-Aware Sharpening
        ↓
 Enhanced Diagnostic Image

Each stage plays a different role in improving the final image quality.

Step 1 — Image Normalization

The first step normalizes image intensities into the range:

[0,1]

This creates a stable numerical space for optimization and transformation.

Normalization also ensures consistency across different X-ray images acquired under varying conditions.

Step 2 — Multi-Otsu Segmentation

Instead of selecting transformation regions manually, I used Multi-Otsu thresholding to initialize intensity breakpoints automatically.

This divides the grayscale intensity range into multiple adaptive regions.

For example:

  • dark regions
  • mid-tone structures
  • bright anatomical regions

Each segment can then be enhanced independently.

Step 3 — Piecewise Linear Transformation (PLT)

This is the core of the framework.

Instead of using one global transformation curve, the image intensity range is divided into multiple linear segments.

Each segment has:

  • its own slope
  • its own local contrast behavior

The transformation is defined as:

Where:

  • $s_i$ controls local contrast amplification within segment i
  • $b_i$ are the segment breakpoints that partition the intensity range
  • The first summation term accumulates contributions from previous segments
  • The denominator normalizes the transformation into the range [0,1]
  • $varepsilon$ is a small constant used to avoid division by zero

This creates a highly flexible enhancement function capable of adapting to complex medical image characteristics.

Why Differential Evolution?

This is where the project became really interesting.

Instead of manually tuning transformation parameters, I used Differential Evolution (DE) to optimize them automatically.

DE is an evolutionary optimization algorithm inspired by natural selection.

It works by:

  • generating candidate solutions
  • mutating parameters
  • recombining candidates
  • selecting the best-performing transformations

Over multiple generations, the algorithm gradually discovers enhancement parameters that maximize image quality.

The Fitness Function

Medical image quality cannot be measured using only one metric.

So I designed a multi-objective fitness function combining:

  • Edge Density
  • Shannon Entropy
  • Structural Similarity (SSIM)
  • Detail-to-Background Variance Ratio

The optimization score becomes:

This allowed the optimizer to balance:

  • detail visibility
  • structural preservation
  • information richness
  • and perceptual quality

simultaneously.

Step 4 — Gamma Refinement

After optimization, gamma correction is applied to refine perceptual brightness.

I discovered experimentally that:

  • lower gamma values improve shadow visibility
  • higher gamma values preserve brightness realism

The best results were usually obtained around:

gamma ≈ 0.7 – 0.9

This stage significantly improved hidden texture visibility in dark X-ray regions.

Step 5 — Edge-Aware Sharpening

Finally, an edge-aware unsharp masking stage enhances structural clarity without introducing excessive artifacts.

This helps:

  • improve edge definition
  • preserve anatomical boundaries
  • enhance perceptual sharpness

while avoiding aggressive oversharpening.

The Most Challenging Part

One of the hardest challenges was avoiding over-enhancement.

In medical imaging, maximizing contrast blindly can actually damage important structures.

Some enhancement methods create visually impressive images that are diagnostically unreliable.

The real challenge was achieving balance.

Not:

  • maximum brightness

But:

  • maximum useful information

Experimental Results

The framework was tested on:

  • real medical X-ray images
  • public grayscale benchmark datasets

The results showed significant improvements in:

  • local contrast
  • texture visibility
  • edge clarity
  • and structural preservation

compared to traditional methods.

Observed Improvements

Metric Traditional Methods Proposed Framework
Edge Preservation Medium High
Local Contrast Medium High
Structural Fidelity Medium Very High
Noise Amplification High Low
Visual Realism Moderate Natural

The most satisfying part was seeing hidden structures become visible without destroying the natural appearance of the X-ray.

What I Learned From This Project

This project taught me that image enhancement is far more complex than “making images brighter”.

I also learned that:

  • optimization algorithms are incredibly powerful outside classical machine learning
  • medical imaging requires structure-aware enhancement
  • adaptive transformations outperform fixed global mappings
  • image quality is inherently multi-objective

Most importantly:

Good enhancement should reveal information — not fabricate it.

Future Improvements

There are many exciting directions to extend this work further:

  • GPU acceleration for real-time processing
  • CNN-assisted enhancement guidance
  • GAN-based perceptual refinement
  • Multi-modal medical imaging support
  • Adaptive parameter learning
  • Clinical evaluation with radiologists

Final Thoughts

Building this framework was one of the most rewarding projects I’ve worked on.

It combined:

  • mathematics
  • optimization
  • computer vision
  • medical imaging
  • and practical engineering

into one system capable of improving the visibility of difficult X-ray images.

And honestly…

Seeing hidden details emerge from low-contrast medical images after optimization felt almost like watching the algorithm “discover” information hidden inside the image.

Tech Stack

  • Python
  • OpenCV
  • NumPy
  • SciPy
  • Differential Evolution
  • Medical Image Processing
  • Evolutionary Optimization

Tags

#python
#ai
#datascience
#computervision
#imageprocessing
#medicalimaging
#optimization

Why I Like Small Software

Building personal utility in the age of AI

One thing I’ve increasingly appreciated about AI-assisted development is how practical it can be for building small software. The world doesn’t need another social media app or crypto-tracker. What the world needs is more actual utility. Not more products or services. Tools.

This is not to say that products and services are bad, far from it. There just aren’t enough personal tools, or tools that can be made personal. Tools that can solve immediate problems and then get out of the way. That side of AI-assisted development gets overlooked. Too many people immediately frame it as a business opportunity instead of a utility. The unfortunate side effect is that people stop seeing small ideas as worth building. Not everything needs to scale.

I’ve been building a handful of tiny local-first utilities for the last several months. Mostly workflow stuff: cleaning pasted text, comparing text differences, organizing snippets, temporary notes. Small things that are immediately useful while working.

What I like about these tools is that many of them don’t really need much infrastructure. No accounts. No subscriptions. No syncing requirements. In some cases, no backend at all. Just a browser tab and local storage.

That kind of software feels strangely rare now.

A lot of modern software feels obligated to become a platform. Even very small utilities often arrive bundled with dashboards, onboarding flows, notifications, analytics, and recurring pricing. Sometimes that makes sense. Often it doesn’t.

I’ve found myself moving in the opposite direction instead. Smaller scope, narrower purpose, less noise. Simplify, simplify.

AI has made that process much easier. Not because it magically builds perfect applications, but because it lowers the friction between “this would be useful” and “this now exists.” AI-assisted development dramatically lowers the barrier to building small software. Too often those ideas become startup pitches or SaaS concepts with the thinking of “what do people need/want” instead of “what do I need/want.”

When I’m sitting at my machine and saying to myself, “sure would be nice to be able to ____ right now,” I can. I can build it quickly. Sometimes immediately. AI-assisted development makes personal utility much more achievable.

That utility changes the kind of software people are willing to make. Not just in terms of using AI, but in not linking databases, not building backend security, no updating API keys. No servers, no accounts, no credit cards. You can build a personal, useful tool that suits your own purposes.

You can build tools purely because they improve your own workflow a little. You can experiment with odd ideas that would never justify traditional development time. You can make software that serves a specific personal need instead of trying to appeal to everyone.

I think there’s real value in that direction, especially when paired with privacy-first or local-first design. Software does not always need to become a service. Sometimes it can just remain a tool.

Not everyone wants to start a software company. Some people just want better tailored tools. And honestly, I’d like to see more of that.

Beyond the Model: Why the Gemini Ecosystem and Google AI Studio Are Redefining Enterprise AI Architecture in 2026

The landscape of Artificial Intelligence in 2026 has officially shifted from the novelty of raw Large Language Model (LLM) capabilities to the pragmatism of ecosystem maturity. During the Google I/O 2026 keynotes, a fundamental theme emerged: Gemini is no longer just a family of models—it is an integrated, end-to-end developer ecosystem.

For senior developers and software architects, the most compelling revelation isn’t just a bump in context windows or token throughput; it is how Google AI Studio has positioned itself as the fastest, most frictionless pipeline to build, test, and scale enterprise-grade applications.

Here is an architectural deep dive into why this paradigm shift matters, along with a critical analysis of what it means for the future of development.

1. The Paradigm Shift: From Isolated Models to Living Ecosystems

For years, the industry treated LLMs as isolated APIs. You sent a payload, received a completion, and managed state, memory, and orchestration entirely on your own infra. Google’s current trajectory explicitly challenges this fragmented approach.

“Gemini is evolving to be more than just the models. It supports an entire ecosystem — from the products that billions of people use every day, to the APIs and platforms helping developers and businesses innovate.”

By embedding Gemini natively within both consumer-facing products and foundational cloud infrastructure, Google minimizes the cognitive load for developers. We are moving away from building brittle wrapper applications and moving toward architecting intelligent orchestration layers where the model inherently understands its environment.

2. Google AI Studio: The Catalyst for Rapid Prototyping

Historically, the transition from an initial prompt engineering experiment to a production-ready system was fraught with friction. Google AI Studio addresses this exact pain point, serving as the fastest gateway to build with Gemini.

Why It Excels in Production Workflows:

  • Frictionless Iteration: It bridges the gap between the “reading the docs” phase and hands-on deployment, allowing developers to test multi-modal prompts under strict latency constraints.
  • Structured Output Control: Modern enterprise systems cannot tolerate non-deterministic JSON schemas. AI Studio’s enhanced constraint configurations guarantee that responses adhere strictly to backend data contracts.
  • System Prompt Efficiency: The platform optimizes how system instructions are cached, drastically reducing time-to-first-token (TTFT) and minimizing computational overhead during heavy API polling.

3. Engineering Analysis: Performance, Security, and Memory Management

When building software designed to handle millions of requests, raw performance and cost efficiency are paramount. The 2026 Gemini ecosystem introduces several vital advantages for enterprise engineering teams:

Architectural Efficiency Matrix

Feature Focus Legacy AI Integration Approach The 2026 Gemini Ecosystem Approach
Context Handling High token overhead; manual chunking required. Native multi-modal context processing with intelligent caching.
Development Speed Complex middleware setup and custom SDK boilerplate. Rapid prototyping directly inside Google AI Studio.
Architectural Scope Disjointed API endpoints with decoupled state management. Unified ecosystem linking consumer products, APIs, and enterprise cloud infrastructure.

Memory & State Management

Managing long-context memory efficiently is one of the highest expenses in production AI applications. With Gemini’s sophisticated contextual handling, developers can pass massive codebases or multi-hour video streams directly into the context window without experiencing the catastrophic drift or high latency penalties common in earlier iterations.

4. A Balanced Critique: The Reality of Ecosystem Lock-In

While the velocity offered by Google AI Studio is unmatched, an objective architectural evaluation requires looking at the trade-offs:

  • The Lock-In Dilemma: The deeper you integrate with specialized ecosystem features, the higher the migration cost becomes if your enterprise requires a multi-cloud or hybrid strategy later on.
  • Ecosystem Dependability: As applications rely more heavily on upstream managed services within Google AI Studio, local debugging becomes increasingly abstract. Teams must invest in robust observability tools to monitor API behavior, rate limits, and latency variances globally.

Conclusion: The Roadmap Ahead

Google I/O 2026 has made one thing abundantly clear: the future belongs to developers who can launch securely, iterate instantly, and build responsibly. Google AI Studio provides the tools to do exactly that, effectively democratizing production-grade AI development.

Whether you are currently refactoring an existing codebase or architecting a greenfield system from scratch, exploring this evolving ecosystem is no longer optional—it is a competitive necessity.

What are your thoughts on the latest updates? Are you planning to migrate your production workloads to Google AI Studio, or are you keeping a hybrid approach? Let’s discuss in the comments below!

How to Make Large Time-Series Charts Smooth in Vue.js + ApexCharts (and fix Zoom & Scroll behavior issues)

Over the last few weeks, I’ve been working on a front-end application project built with Vue.js and ApexCharts that displays time-series data about different sensors installed in different rooms and buildings.

ApexCharts is an excellent library for creating interactive charts, and integrating it in [Vue.js (https://vuejs.org) is really a piece of cake. However, when it comes to displaying a time-series chart with thousands of points, the performance can suffer, sometimes causing the page to freeze during the rendering or when the user zooms or navigates through the data.

Downsampling data

To solve this issue, it is recommended to downsample the data to reduce the number of displayed points, especially when the selected time range is large enough that highly granular data is unnecessary. If the user later needs to inspect detailed data, the application can switch back to raw data after zooming in.

The criteria for switching between downsampled and raw data depends on the use case. It can be based on the zoom level, the selected date range, or the number of points to display.

The problem

Switching between raw and downsampled data also requires filtering the points to display according to the selected time range. And this is where the problem appears: once you filter data, the zooming and scrolling events of the chart stop working properly when using the mouse scroll. In the era of AI and agent-based coding, solving the issue might seem trivial. But after several attempts and different interactions, I could not find a complete working example that fully
solved the problem.

The solution

Here, I would like to propose a solution that includes the use of downsampled data when the number of displayed points is high enough to avoid performance issues with a line chart and switches to raw data otherwise. The solution is based on the zoomed, scrolled and beforeResetZoom chart events while still relying on the reactive state of the Vue.js component.

To make the solution work correctly, it is also necessary to include a hidden dataset containing the minimum and maximum dates of the complete time series, both with null values. This hidden dataset allows the chart to zoom out completely and pan across the entire x-axis using both the chart controls and the mouse wheel.

Without this additional dataset, these interactions only work correctly when using the built-in chart buttons.

How to do it?

The setup

In a real application, the data usually comes from an API or another external source. After loading the data, each dataset can be downsampled and stored in the component’s reactive state.

Alternatively, downsampling could also be performed in the backend before sending the data to the frontend, depending on how much control you have over the application architecture.

For this example, I generate three random temperature datasets with 10,000 points each and downsample them using the Largest-Triangle Three-Buckets (LTTB) algorithm provided by the downsample package, reducing each dataset to 700 points.

import {LTTB} from 'downsample/methods/LTTB';

let northStationData = generateTemperaturePoints({
    startDate: new Date('2026-01-01T00:00:00'),
    count: 10000,
    intervalMs: 60 * 1000,
    baseTemp: 15,
    dailyAmplitude: 6.5,
    noise: 1.1,
    warmingTrendPerHour: 0.0021
});

let northStationLTTB = LTTB(northStationData, 700);

We also need to store the minimum and maximum dates across all datasets, since they will later be used to create the hidden dataset.

const allX = [northStationData, cityCenterData, southStationData].flatMap(s => s.map(p => p.x));

let minimumDate = Math.min(...allX);
let maximumDate = Math.max(...allX);

this.ranges = {
    minimumDate: minimumDate,
    maximumDate: maximumDate,
};

Computed properties

Dataset with minimum and maximum dates

Once the minimum and maximum dates are stored in the reactive state, we can create a computed property that returns a dataset containing only those boundary dates.

plotMinMaxDataset()
{
    return {
        name: 'rangeMinMaxInvisible',
        data: [
            {x: new Date(this.ranges.minimumDate).getTime(), y: null},
            {x: new Date(this.ranges.maximumDate).getTime(), y: null},
        ],
        showInLegend: false,
    };
}

Initial datasets

When displaying the chart for the first time, we can create another computed property that returns the downsampled datasets together with the hidden boundary dataset.

plotSeries()
{
    let seriesData = [];
    Object.keys(this.data).forEach(key => {
        seriesData.push({
            name: `${this.data[key].label} (downsampled)`,
            data: toRaw(this.data[key].downsample),
        })
    });

    /**
     * Add the minimum and maximum points available in the datasets
     * to avoid zooming and scrolling to lose the minimum and maximum ranges.
     */
    seriesData.push(toRaw(this.plotMinMaxDataset));
    return seriesData;
}

Methods

Switching between raw and downsampled data

This method is called whenever the user zooms or scrolls through the chart. It filters the data according to the selected range and switches between raw and downsampled datasets based on a defined criterion. At the end, it also includes the dataset with the minimum and maximum dates to avoid zooming and scrolling to lose the original boundaries.

In this example, the criterion is the number of points available in the filtered raw dataset.

plotRangeOnChange(chartContext, minDate, maxDate)
{
    let dataUpdate = [];

    Object.keys(this.data).forEach(key => {
        let dataTypeName = 'raw'
        let dataRaw = pointFilter(this.data[key].raw, minDate, maxDate);

        /**
         * Based on the number of points in the raw dataset after filtering,
         * decide whether to keep raw or downsampled datasets.
         *
         * The criteria in this case is to verify the number of points, but it could
         * also be the range of dates.
         */
        if (dataRaw.length > 1000) {
            dataRaw = pointFilter(this.data[key].downsample, minDate, maxDate);
            dataTypeName = 'downsampled';
        }

        dataUpdate.push({
            name: `${this.data[key].label} (${dataTypeName})`,
            data: toRaw(dataRaw),
        });
    });

    /**
     * Add the minimum and maximum points available in the datasets
     * to avoid zooming and scrolling to lose the minimum and maximum ranges.
     */
    dataUpdate.push(toRaw(this.plotMinMaxDataset));

    chartContext.updateOptions({
        series: toRaw(dataUpdate),
        xaxis: {
            min: minDate,
            max: maxDate,
        }
    });
}

Configuring the chart

We need to configure the zoomed and scrolled events to trigger the switch between raw and downsampled data using the method defined above. We also configure the beforeResetZoom event to restore the chart to the full available date range.

These events provide:

  1. chartContext, which gives access to the chart instance.
  2. xaxis, which contains the currently visible minimum and maximum dates.

At this stage, we also hide the auxiliary hidden dataset from the legend and remove its marker by filtering the series name in the legend.formatter of the plot options and setting its marker size to 0.

let markersSize = [];
Object.keys(this.data).forEach(key => {
    markersSize.push(8);
});

markersSize.push(0);

let plotOptions = {
    chart: {
        id: 'vuechart-example',
        events: {
            /**
             * Update data based on the selected range in the x-axis when events occur
             */
            zoomed: (chartContext, {xaxis}) => this.plotRangeOnChange(chartContext, xaxis.min, xaxis.max),
            scrolled: (chartContext, {xaxis}) => this.plotRangeOnChange(chartContext, xaxis.min, xaxis.max),
            beforeResetZoom: (chartContext) => this.plotRangeOnChange(chartContext, null, null),
        },
    },
    legend: {
        formatter: function (seriesName) {
            /**
             * Hide the marker containing the minimum and maximum values in the x-axis range
             */
            return seriesName === 'rangeMinMaxInvisible' ? null : seriesName;
        },
        markers: {
            size: markersSize
        },
    },
    /* ... */
}

Wrapping up

In this article, I showed how to use downsampling data in a Vue.js + ApexCharts application to reduce the number of points to display and improve the user experience, while still allowing to zoom in, zoom out and scroll over the chart without encountering problems with the mouse wheel.

The complete code of this example is available in the GitHub repository vue-apexcharts-downsample-zoom.

You can also try the live demo here.

Leave a comment if you have any questions or suggestions to improve, I’ll be happy to interact with you!

Let’s make the world a better place together, one snippet at a time!