<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>AppFlow | John Nessime</title>
	<atom:link href="https://john-nessime.com/blog/tag/appflow/feed/" rel="self" type="application/rss+xml" />
	<link>https://john-nessime.com/blog/tag/appflow/</link>
	<description>Cloud, DevOps, Data &#38; AI — Built, Tested, Explained</description>
	<lastBuildDate>Sun, 02 Aug 2026 10:48:17 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>

<image>
	<url>https://john-nessime.com/blog/wp-content/uploads/2026/07/cropped-jn-32x32.png</url>
	<title>AppFlow | John Nessime</title>
	<link>https://john-nessime.com/blog/tag/appflow/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Cheapest at Two Flows, Worst at Forty: AppFlow vs Lambda vs MuleSoft on Cost</title>
		<link>https://john-nessime.com/blog/devops/appflow-vs-lambda-vs-mulesoft/</link>
					<comments>https://john-nessime.com/blog/devops/appflow-vs-lambda-vs-mulesoft/#respond</comments>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Mon, 03 Aug 2026 08:47:58 +0000</pubDate>
				<category><![CDATA[Cloud Computing]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[AppFlow]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWS Lambda]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Cost Optimization]]></category>
		<category><![CDATA[Data Integration]]></category>
		<category><![CDATA[ETL]]></category>
		<category><![CDATA[FinOps]]></category>
		<category><![CDATA[Infrastructure]]></category>
		<category><![CDATA[iPaaS]]></category>
		<category><![CDATA[MuleSoft]]></category>
		<category><![CDATA[Salesforce]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=103</guid>

					<description><![CDATA[<p>AppFlow won the spreadsheet at two integrations. Eighteen months and forty flows later it's a line item that gets mentioned in the monthly review. These three options aren't three prices, they're three cost shapes, and the crossover is the whole decision.</p>
<p>The post <a href="https://john-nessime.com/blog/devops/appflow-vs-lambda-vs-mulesoft/">Cheapest at Two Flows, Worst at Forty: AppFlow vs Lambda vs MuleSoft on Cost</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The spreadsheet was convincing. Two Salesforce integrations, a daily schedule, a few hundred megabytes a month. AppFlow came out cheapest by a wide margin, nobody had to write any code, and the decision took an afternoon.</p>



<p class="wp-block-paragraph">Eighteen months later there are forty flows, several of them on five-minute schedules because someone wanted fresher dashboards, and AppFlow is a line item that gets mentioned in the monthly review. Nothing was done wrong. The spreadsheet modelled the situation at the moment of the decision and that situation changed.</p>



<p class="wp-block-paragraph">This is what makes an honest <strong>AppFlow vs Lambda vs MuleSoft</strong> cost comparison hard: you are not comparing three prices, you are comparing three different <em>shapes</em>, and the cheapest one at two integrations is frequently not the cheapest one at forty. The crossover is the entire decision, and almost nobody models it.</p>



<p class="wp-block-paragraph">A note on numbers before we start. I am not quoting rate cards. AWS pricing varies by region and changes, MuleSoft publishes no list prices at all, and every comparison article with a neat table of dollar figures is quietly out of date. What does not change is the <em>mechanism</em> each one bills on, and that is what determines whether your bill grows with volume, with integration count, or not at all.</p>



<h2 class="wp-block-heading">Three shapes</h2>



<ul class="wp-block-list">
<li><strong>AppFlow</strong> is pure consumption with no floor. You pay per flow run and per gigabyte processed. Two flows cost almost nothing; the bill scales with flows multiplied by frequency.</li>
<li><strong>Lambda</strong> is consumption too, but the meter is nearly irrelevant. The real cost is engineering time to build and own it, and the surrounding AWS services it drags along.</li>
<li><strong>MuleSoft</strong> is a floor. An annual contract sized on capacity or usage entitlements, paid whether you use it or not, largely independent of how many integrations you run inside it.</li>
</ul>



<p class="wp-block-paragraph">Draw those as lines against integration count and you get the whole argument. One starts near zero and climbs steeply. One starts moderate and climbs gently. One starts high and is close to flat. Where they cross depends entirely on your situation, which is why a generic answer is worthless and a framework is not.</p>



<h2 class="wp-block-heading">AppFlow: cheap until the schedule tightens</h2>



<p class="wp-block-paragraph">AppFlow bills per successful flow run plus data processed. The detail that catches people, and it is stated plainly in AWS&#8217;s own pricing documentation, is this: <strong>a flow run that checks for new data and finds none still counts.</strong> You pay for polling, not for movement.</p>



<p class="wp-block-paragraph">That turns schedule frequency into the dominant variable, and it is the one nobody models because at decision time everything is daily:</p>



<pre class="wp-block-code"><code>Flow runs per month, per flow:

  every 5 minutes   12 x 24 x 30  =  8,640
  every 15 minutes   4 x 24 x 30  =  2,880
  hourly                 24 x 30  =    720
  daily                       30  =     30

Twenty flows, five-minute schedule:  172,800 runs
The same twenty flows, hourly:        14,400 runs

Identical data. Twelve times the flow run charge.</code></pre>



<p class="wp-block-paragraph">Two more things that are not on the AppFlow line item. Standard S3 request and storage charges apply to whatever it writes, and KMS charges apply to the keys it uses. And on some connectors, running additional concurrent processes to speed a transfer is billed as additional flows.</p>



<p class="wp-block-paragraph"><strong>Where AppFlow wins:</strong> a small number of standard SaaS-to-AWS movements on relaxed schedules, where nobody wants to own code. It is genuinely excellent at that, it goes live in an afternoon, and there is no floor to justify.</p>



<p class="wp-block-paragraph"><strong>Where it stops winning:</strong> many flows, tight schedules, or transformation logic it cannot express. Schema drift handling is basic, and there is a point where you are working around the tool more than using it.</p>



<h2 class="wp-block-heading">Lambda: the invoice is not the cost</h2>



<p class="wp-block-paragraph">For CRM-sized workloads, Lambda&#8217;s compute charge is close to a rounding error. A function that runs hourly for thirty seconds costs so little that optimising it is a waste of an afternoon.</p>



<p class="wp-block-paragraph">The costs are elsewhere, and two of them are large.</p>



<p class="wp-block-paragraph"><strong>The surrounding AWS services.</strong> NAT gateway data processing if the function sits in a VPC, CloudWatch Logs ingestion, S3 requests from small-file writes, Athena scans downstream. On a real pipeline these routinely exceed the compute charge several times over, and none of them appear under &#8220;Lambda&#8221; in Cost Explorer.</p>



<p class="wp-block-paragraph"><strong>Engineering time, forever.</strong> Not just the build. Pagination, retries, watermarks, delete handling, schema drift, credential rotation, alerting, and someone available when it breaks at an awkward hour. This is the line that gets costed at zero in every comparison and is usually the largest number in the exercise.</p>



<p class="wp-block-paragraph">The interesting property, though, is that the marginal cost of integration number eleven is much lower than integration number one, <em>if</em> you built a framework rather than eleven scripts. Shared extraction, shared state, shared error handling, one deployment pipeline. That is what makes the Lambda line climb gently instead of steeply. Teams that build eleven bespoke functions get the worst of both worlds: consumption pricing with none of the amortisation.</p>



<p class="wp-block-paragraph"><strong>Where Lambda wins:</strong> you already have AWS engineers, you need control over the output shape, and the integration count is going up. Also anywhere the requirement is genuinely unusual, because a managed connector cannot express what it does not have a checkbox for.</p>



<p class="wp-block-paragraph"><strong>Where it does not:</strong> a team of two who would rather ship product, or an organisation where the integration needs to be maintainable by people who are not engineers.</p>



<h2 class="wp-block-heading">MuleSoft: a floor, and what it buys</h2>



<p class="wp-block-paragraph">MuleSoft publishes no list prices. Everything is quote-based, annual, and negotiated, which means any figure you find online is somebody else&#8217;s contract and tells you little about yours. It is also worth knowing that the commercial model has been repackaged relatively recently, moving new customers away from the older capacity-based structure toward usage entitlements, so a pricing article from a couple of years ago may describe a product you cannot buy.</p>



<p class="wp-block-paragraph">Three structural things matter more than the number.</p>



<p class="wp-block-paragraph"><strong>It is a commitment, not a meter.</strong> You size it up front and pay for it regardless of use, which is bad if you are unsure and fine if you are certain. Undersizing means overages; oversizing means paying for headroom.</p>



<p class="wp-block-paragraph"><strong>Separate meters do not offset each other.</strong> Integration capacity, API traffic and add-on modules are metered independently, so spare room in one does not cover a spike in another. Model each axis separately or the renewal will be a surprise.</p>



<p class="wp-block-paragraph"><strong>Renewal escalation is normal.</strong> Enterprise contracts typically carry annual uplift clauses, so the three-year total is meaningfully above three times year one. Negotiate that at signature, because the leverage is gone afterwards.</p>



<p class="wp-block-paragraph">Now the fair part, because a post that treats MuleSoft as merely expensive is not being honest. What the floor actually buys: API management and governance as a first-class capability rather than something you assemble; a connector catalogue covering legacy and on-premise systems that neither AWS option touches; integrations that a trained analyst can maintain without a software team; audit trails, support contracts and SLAs that satisfy people who ask for them in writing. At a hundred integrations across several business units, the fixed cost is spread thin and the governance is the point.</p>



<p class="wp-block-paragraph">The cost that is easy to miss is skills. DataWeave and the Anypoint tooling are proprietary, so you are hiring for a specific platform and your switching cost rises with every integration you build. That is not an argument against it; it is an argument for deciding deliberately rather than drifting in.</p>



<p class="wp-block-paragraph"><strong>Where MuleSoft wins:</strong> many integrations, many teams, on-premise or legacy systems in the mix, and governance requirements with a compliance function attached to them.</p>



<p class="wp-block-paragraph"><strong>Where it does not:</strong> three integrations, one team, all cloud. The floor will never amortise and you will spend the first year justifying it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Modelling the crossover</h2>



<p class="wp-block-paragraph">Build the comparison over three years rather than one month, and project the inputs rather than freezing them.</p>



<ol class="wp-block-list">
<li><strong>Count integrations at year three, not today.</strong> Ask the people requesting them. The answer is always higher than the current number and it is the variable the whole model turns on.</li>
<li><strong>Write down the schedule for each one, and defend it.</strong> &#8220;Every five minutes&#8221; is usually a preference, not a requirement. For AppFlow this is the difference between a small bill and a large one.</li>
<li><strong>For AppFlow, multiply flows by runs per month.</strong> Apply current rates yourself, then add S3 requests and KMS.</li>
<li><strong>For Lambda, cost the engineering explicitly.</strong> Build days plus ongoing maintenance days per year, at a loaded rate. If that number embarrasses you, it is probably the accurate one.</li>
<li><strong>For MuleSoft, get an actual quote</strong>, model each meter separately, and include renewal escalation across the term.</li>
<li><strong>Add the cost of being wrong.</strong> How much does switching cost in each direction? Consumption services you can walk away from. An annual contract and a proprietary skill set you cannot.</li>
</ol>



<p class="wp-block-paragraph">Then plot all three. You are looking for where the lines cross and whether your projected integration count sits comfortably on one side of it or uncomfortably near it. Near a crossover, pick the option that is cheaper to reverse.</p>



<h2 class="wp-block-heading">Common mistakes</h2>



<ul class="wp-block-list">
<li>Comparing one month at today&#8217;s integration count.</li>
<li>Costing engineering time at zero on the build-it-yourself option.</li>
<li>Not modelling flow run frequency, which is the single largest AppFlow variable.</li>
<li>Forgetting that AppFlow charges for polling runs that find nothing.</li>
<li>Attributing only the Lambda invoice to the Lambda option, ignoring NAT, logs and S3 requests.</li>
<li>Building eleven bespoke Lambdas instead of one framework, so nothing amortises.</li>
<li>Using published MuleSoft prices from an article, when no list prices exist and the packaging has changed.</li>
<li>Ignoring renewal escalation when comparing an annual contract against consumption pricing.</li>
<li>Treating a hybrid as a failure of nerve rather than the usual right answer.</li>
<li>Choosing for the architecture you want rather than the team you have.</li>
</ul>



<h2 class="wp-block-heading">How I&#8217;d decide</h2>



<p class="wp-block-paragraph">Start with AppFlow if the integrations are standard, few, and not urgent. It is the lowest-commitment option and you can leave whenever you like.</p>



<p class="wp-block-paragraph">Move to Lambda when you have AWS engineers, the count is climbing, and you need control over the output. Build it once as a framework, not repeatedly as scripts, or you get the cost profile without the benefit.</p>



<p class="wp-block-paragraph">Consider MuleSoft when the problem is organisational rather than technical: many teams, legacy systems, governance requirements, and integrations that need to outlive the person who built them.</p>



<p class="wp-block-paragraph">And be relaxed about mixing them. Plenty of sensible estates run AppFlow for the boring standard movements and custom code for the two integrations that are genuinely specific. Insisting on one tool for everything is a preference, not an architecture.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">FAQ</h2>



<h3 class="wp-block-heading">Which is cheapest overall?</h3>



<p class="wp-block-paragraph">None of them, universally. AppFlow is cheapest at low integration counts and relaxed schedules. Lambda is cheapest at scale if you already have the engineers and build a shared framework. MuleSoft is cheapest only when the fixed cost is spread across many integrations and teams. The question is which shape fits your trajectory.</p>



<h3 class="wp-block-heading">Why did my AppFlow bill grow when data volume didn&#8217;t?</h3>



<p class="wp-block-paragraph">Because you are billed per flow run, including runs that check for new data and find none. Somebody tightened a schedule, or the flow count grew. Both multiply the run charge without moving a single extra record.</p>



<h3 class="wp-block-heading">Is Lambda really cheaper than AppFlow?</h3>



<p class="wp-block-paragraph">On the invoice, usually yes. In total cost, only once the engineering amortises across several integrations. For one or two simple flows, AppFlow is almost always the better economic answer, and the &#8220;we&#8217;ll just write a Lambda&#8221; instinct is a false economy at that scale.</p>



<h3 class="wp-block-heading">Why can&#8217;t I find MuleSoft&#8217;s prices?</h3>



<p class="wp-block-paragraph">Because there aren&#8217;t any published. It is sold through quotes with negotiated discounts, so figures you find online are other people&#8217;s contracts. Get a quote, model each meter separately, and negotiate the renewal escalation before you sign rather than after.</p>



<h3 class="wp-block-heading">Can I start on one and move later?</h3>



<p class="wp-block-paragraph">Between the two consumption options, fairly easily, especially if your destination schema is defined by you rather than by the tool. Off an annual contract with proprietary tooling, much less easily, which is exactly why the switching cost belongs in the model rather than as a footnote.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The one thing to remember</h2>



<p class="wp-block-paragraph">You are not choosing the cheapest tool. You are choosing a cost shape, and committing to how it behaves as your integration count and your schedules change over the next three years.</p>



<p class="wp-block-paragraph">So model the trajectory, not the snapshot. Put engineering time in the spreadsheet at a real rate. And when the lines are close together, take the option you can walk away from, because being wrong cheaply is worth more than being right narrowly.</p>



<h2 class="wp-block-heading">Need the model built for your numbers?</h2>



<p class="wp-block-paragraph">This decision usually gets made under time pressure with incomplete inputs, and then lived with for years. Work I take on:</p>



<ul class="wp-block-list">
<li>Building the three-year cost model for your actual integration inventory, schedules and volumes, with engineering time costed honestly.</li>
<li>Auditing an existing AppFlow estate for flow count, schedule frequency and the charges that sit outside the AppFlow line item.</li>
<li>Designing a shared extraction framework so custom integrations amortise instead of multiplying.</li>
<li>Migrating between approaches without downtime, including keeping the destination schema stable so the choice stays reversible.</li>
<li>Reviewing an integration platform contract for meter structure, overage terms and renewal escalation before you sign.</li>
<li>Hybrid architectures where managed connectors handle the standard movements and code handles the awkward ones.</li>
</ul>



<p class="wp-block-paragraph">Send me your integration list with schedules and rough volumes, and I will tell you which shape you are actually buying.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/devops/appflow-vs-lambda-vs-mulesoft/">Cheapest at Two Flows, Worst at Forty: AppFlow vs Lambda vs MuleSoft on Cost</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://john-nessime.com/blog/devops/appflow-vs-lambda-vs-mulesoft/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The Row Count That Only Goes Up: Building a Salesforce Data Lake on AWS</title>
		<link>https://john-nessime.com/blog/devops/salesforce-data-lake-aws/</link>
					<comments>https://john-nessime.com/blog/devops/salesforce-data-lake-aws/#respond</comments>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Sun, 02 Aug 2026 11:01:00 +0000</pubDate>
				<category><![CDATA[Cloud Computing]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[Amazon Athena]]></category>
		<category><![CDATA[Amazon S3]]></category>
		<category><![CDATA[Apache Iceberg]]></category>
		<category><![CDATA[AppFlow]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWS Glue]]></category>
		<category><![CDATA[Change Data Capture]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Data Integration]]></category>
		<category><![CDATA[Data Lake]]></category>
		<category><![CDATA[Data Warehouse]]></category>
		<category><![CDATA[ETL]]></category>
		<category><![CDATA[Infrastructure]]></category>
		<category><![CDATA[Salesforce]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[Troubleshooting]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=83</guid>

					<description><![CDATA[<p>Your dashboard shows four percent more opportunities than Salesforce, and the extra rows were all deleted months ago. Incremental sync on SystemModstamp can never see a record that no longer exists. Here's how to build a Salesforce data lake on AWS that stays equal to the source.</p>
<p>The post <a href="https://john-nessime.com/blog/devops/salesforce-data-lake-aws/">The Row Count That Only Goes Up: Building a Salesforce Data Lake on AWS</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Someone in RevOps mentions that the pipeline dashboard shows about four percent more opportunities than Salesforce does. Not wildly wrong. Wrong enough that nobody trusts the number in a board meeting.</p>



<p class="wp-block-paragraph">You pull a sample of the extra rows and they all look fine. Real opportunity IDs, real amounts, real owners. You paste one into Salesforce and get nothing. The record was deleted eight months ago. So were the others.</p>



<p class="wp-block-paragraph">Your incremental extract has never deleted a row in its life. It asks Salesforce for everything where <code>SystemModstamp</code> is greater than the last run, and a record that no longer exists cannot come back in that result set. So the lake only ever grows, the gap widens a little every week, and nothing anywhere reports an error.</p>



<p class="wp-block-paragraph">That is the defining problem when you <strong>build a Salesforce data lake on AWS</strong>, and it is barely mentioned in the tutorials, which mostly stop once data lands in S3. Landing the data is the easy part. Keeping it equal to the source is the work.</p>



<p class="wp-block-paragraph">This covers the ingestion options and their honest trade-offs, how to structure the lake, how to handle deletes properly (harder than it sounds), the fields that go stale without telling you, staying inside your API budget, and the query layer on top.</p>



<h2 class="wp-block-heading">Why Salesforce is an awkward source</h2>



<p class="wp-block-paragraph">Four properties make it different from replicating a database, and every design decision below follows from them.</p>



<ul class="wp-block-list">
<li><strong>Deletes are soft, then permanent.</strong> A deleted record moves to the Recycle Bin, where it is visible for a limited window and then gone. Your ability to detect a deletion expires.</li>
<li><strong>Some fields are computed at read time.</strong> Formula fields are not stored. Salesforce works them out when you query, which means what you extracted is a snapshot, not a value that stays true.</li>
<li><strong>The API is a metered resource.</strong> Your org has a daily request allowance shared with every other integration. A careless extract can starve the tools sales actually uses.</li>
<li><strong>The schema changes without warning.</strong> Admins add fields as part of their normal job. Nobody tells the data team.</li>
</ul>



<h2 class="wp-block-heading">Choosing how to get the data out</h2>



<p class="wp-block-paragraph">Four realistic paths, roughly in order of how much you have to operate yourself.</p>



<p class="wp-block-paragraph"><strong>Amazon AppFlow</strong> is the native option: a managed connector that moves Salesforce objects into S3 on a schedule or on events, with no infrastructure to run. It handles both scheduled batch extracts and Salesforce Change Data Capture event flows, and it supports AWS PrivateLink to Salesforce, so the traffic does not have to cross the public internet. That last point matters more than it usually gets credit for.</p>



<p class="wp-block-paragraph">Where it gets thin: schema drift handling is basic, and mapping large numbers of objects through the console gets tedious fast. Define the flows in Terraform or CloudFormation from the start rather than clicking them, or you will end up with production configuration nobody can reproduce.</p>



<p class="wp-block-paragraph"><strong>A managed ELT vendor</strong> such as Fivetran or Airbyte handles schema drift, deletes and incremental logic for you, which is genuinely most of the hard work in this post. You pay per row or per connector, and on a large Salesforce org that number gets attention. Worth pricing against the engineering time before dismissing it.</p>



<p class="wp-block-paragraph"><strong>Rolling your own on the Bulk API</strong> gives you total control and makes you responsible for everything: pagination, retries, API budget, delete detection, schema evolution. I would only choose this when an existing tool genuinely cannot express what you need, and I would expect it to be a real service with monitoring rather than a Lambda someone wrote in an afternoon.</p>



<p class="wp-block-paragraph"><strong>Zero-copy sharing via Salesforce Data Cloud</strong> avoids replication entirely by querying Salesforce-held data from your warehouse. Attractive when compliance objects to copying CRM data, and it carries its own licensing and latency questions. Check what your Salesforce contract actually includes before designing around it.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Structure the lake so mistakes are cheap</h2>



<p class="wp-block-paragraph">Two layers, and the discipline to keep them separate.</p>



<p class="wp-block-paragraph"><strong>Raw</strong> is append-only and never edited. Every extract lands as a new partition, in Parquet, exactly as Salesforce returned it. You do not deduplicate here, you do not fix types here, and you certainly do not delete here. Raw is your ability to rebuild everything downstream when you discover a logic bug six months in, which you will.</p>



<pre class="wp-block-code"><code># Partition by extraction date, not by a business date. A bad run
# is then one partition to drop rather than a full-table repair.
s3://acme-lake/raw/salesforce/opportunity/extract_date=YYYY-MM-DD/</code></pre>



<p class="wp-block-paragraph"><strong>Curated</strong> is what people query: deduplicated to current state, typed properly, deletes applied. Build it as views over raw to begin with, and only materialise the ones that are genuinely too slow. Materialising early gives you a second copy of the truth to keep in sync, which is the problem you are already trying to solve.</p>



<pre class="wp-block-code"><code>-- Current state from an append-only raw layer: keep the most
-- recent version of each record and drop anything flagged deleted.
CREATE OR REPLACE VIEW curated.opportunity AS
SELECT *
FROM (
    SELECT o.*,
           ROW_NUMBER() OVER (
               PARTITION BY id ORDER BY systemmodstamp DESC
           ) AS rn
    FROM raw.opportunity o
)
WHERE rn = 1
  AND is_deleted = false;</code></pre>



<p class="wp-block-paragraph">Register both in the Glue Data Catalog so Athena and Redshift Spectrum see the same definitions. If you expect frequent updates and deletes rather than pure appends, an open table format like Apache Iceberg is worth the extra setup: row-level deletes, schema evolution and time travel are exactly the operations this workload needs, and they are painful to hand-roll on plain Parquet.</p>



<h2 class="wp-block-heading">Handling deletes properly</h2>



<p class="wp-block-paragraph">Here is the section that matters. There are three mechanisms and you need more than one, because each has a hole.</p>



<h3 class="wp-block-heading">1. Query the Recycle Bin</h3>



<p class="wp-block-paragraph">Standard SOQL hides deleted records. The REST API&#8217;s <code>queryAll</code> endpoint, the SOAP <code>queryAll()</code> call, or <code>ALL ROWS</code> in Apex all return them, and <code>IsDeleted</code> exists on virtually every object even though it does not appear in Setup.</p>



<pre class="wp-block-code"><code>-- Against /services/data/vXX.X/queryAll, not /query.
-- Substitute your own high-water mark for the placeholder.
SELECT Id, IsDeleted, SystemModstamp
FROM Opportunity
WHERE IsDeleted = true
  AND SystemModstamp &gt; {last_run_utc}</code></pre>



<p class="wp-block-paragraph">The hole: records sit in the Recycle Bin for fifteen days by default. Salesforce Classic offers an extended retention setting that pushes it to thirty. But the bin also has a capacity tied to your org&#8217;s storage allocation, and when it fills, the oldest records are purged early to make room. No alert, no warning. So your detection window is not fifteen days. It is fifteen days <em>or less</em>, and you find out which during a mass-delete week.</p>



<h3 class="wp-block-heading">2. Change Data Capture</h3>



<p class="wp-block-paragraph">Salesforce CDC publishes create, update, delete and undelete events as they happen, and AppFlow can consume them. This is the cleanest mechanism, because a delete arrives as an event rather than being inferred from an absence.</p>



<p class="wp-block-paragraph">The hole: it is a streaming subscription, so if your consumer is down long enough, you miss events. Event replay windows are finite. CDC is a good primary mechanism and a bad only mechanism.</p>



<h3 class="wp-block-heading">3. Periodic full ID reconciliation</h3>



<p class="wp-block-paragraph">This is the safety net, it catches everything the other two miss, and it is the one people skip because it feels crude. Pull nothing but the <code>Id</code> column for the whole object. One narrow field over the Bulk API is cheap even on millions of rows.</p>



<pre class="wp-block-code"><code>SELECT Id FROM Opportunity</code></pre>



<p class="wp-block-paragraph">Then diff it against the lake. Whatever is on your side and not on theirs no longer exists, whatever the reason:</p>



<pre class="wp-block-code"><code>-- Rows the lake still believes in and Salesforce does not.
SELECT l.id
FROM curated.opportunity l
LEFT JOIN staging.opportunity_ids s
       ON s.id = l.id
WHERE s.id IS NULL;</code></pre>



<p class="wp-block-paragraph">Run it weekly on your important objects. Soft-delete the differences in the curated layer rather than hard-deleting from raw, so you keep the audit trail.</p>



<p class="wp-block-paragraph">One more case this catches that nothing else does cleanly: <strong>merges</strong>. When an admin merges two Accounts, Contacts or Leads, the losing record is deleted and gets a <code>MasterRecordId</code> pointing at the survivor. If your lake keeps the loser, you are double-counting a customer, and the arithmetic is wrong in a way that looks like a data quality problem rather than a pipeline problem.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The other thing that goes stale silently</h2>



<p class="wp-block-paragraph">Formula fields are not stored anywhere. Salesforce computes them when you ask. So the value that landed in your lake is a snapshot from extraction time, and Salesforce will happily return something different tomorrow without the record&#8217;s <code>SystemModstamp</code> moving at all.</p>



<p class="wp-block-paragraph">Cross-object formulas are the worst version. A formula on Opportunity that references a field on its Account recalculates when the Account changes. The Opportunity did not change, so your incremental extract never picks it up. That column in your lake can be wrong indefinitely and nothing detects it.</p>



<p class="wp-block-paragraph">Three ways out, in order of preference:</p>



<ol class="wp-block-list">
<li><strong>Do not extract them.</strong> Pull the underlying fields and reimplement the calculation in your transformation layer, where it is version-controlled and testable. More work up front, correct forever after.</li>
<li><strong>Refresh them on a schedule.</strong> A narrow periodic extract of just the ID plus the formula columns, full-table. Cheap if the column list is short.</li>
<li><strong>Accept it and document it.</strong> Fine for a formula nobody reports on. Not fine for anything in a revenue calculation.</li>
</ol>



<p class="wp-block-paragraph">Keep a list of which columns in your lake are formula-derived. Six months from now, when a number disagrees with Salesforce, that list is the first thing you will want and the last thing anybody wrote down.</p>



<h2 class="wp-block-heading">Stay inside the API budget</h2>



<p class="wp-block-paragraph">Your Salesforce org has a daily API request allowance determined by edition and licence count, and it is shared. Exceed it and everything integrated with Salesforce stops working, not just your pipeline. That includes whatever marketing and support depend on, which is a conversation you would rather not have.</p>



<ul class="wp-block-list">
<li><strong>Use the Bulk API for extracts</strong>, not the REST query API. Bulk is designed for volume and consumes the allowance very differently.</li>
<li><strong>Incremental by default, full refresh by exception.</strong> A nightly full extract of a large object is the single most common way to blow the budget.</li>
<li><strong>Give the pipeline its own integration user.</strong> That way the API usage reports attribute consumption to it, and you can see your own footprint instead of arguing about it.</li>
<li><strong>Watch consumption as a metric.</strong> Salesforce exposes API usage; graph it. Finding out by outage is expensive.</li>
</ul>



<p class="wp-block-paragraph">One thing that surprises people: field-level security on that integration user determines which fields come back. A field can exist in Salesforce, be populated, and arrive in your lake as consistently null because the integration user&#8217;s profile cannot see it. Check the profile before debugging the pipeline.</p>



<h2 class="wp-block-heading">Query layer and access</h2>



<p class="wp-block-paragraph">Athena over the Glue catalog is the default answer and a good one: no cluster to run, pay per query, and it reads the curated views directly. Redshift Spectrum makes sense when you already run Redshift and want to join CRM data to warehouse tables.</p>



<p class="wp-block-paragraph">Cost control on Athena is mostly about scan volume, which means partitioning and columnar storage rather than query tuning. Parquet plus sensible partitions does more than any amount of SQL cleverness.</p>



<p class="wp-block-paragraph">On access: you have just replicated your customer database into object storage. Contacts and Leads are personal data, and Salesforce&#8217;s own field-level security does not follow it across. Encrypt the bucket, keep it private, use Lake Formation for column-level grants if different teams need different views, and decide the retention policy deliberately rather than by default. &#8220;We keep everything forever in S3 because it&#8217;s cheap&#8221; is a defensible engineering position and a poor compliance one.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<h3 class="wp-block-heading">Row counts drift upward over time</h3>



<p class="wp-block-paragraph">Deletes are not being applied. Run the full ID reconciliation and see how many rows come back. If it is a lot, also check for merged records via <code>MasterRecordId</code> before assuming the extract is at fault.</p>



<h3 class="wp-block-heading">A column is always null in the lake, populated in Salesforce</h3>



<p class="wp-block-paragraph">Field-level security on the integration user&#8217;s profile. Log in as that user, or check the profile&#8217;s field permissions, before touching the pipeline.</p>



<h3 class="wp-block-heading">A value disagrees with Salesforce but the record looks current</h3>



<p class="wp-block-paragraph">Almost certainly a formula field, especially a cross-object one. Check whether the column is formula-derived; if it is, the record&#8217;s modstamp never moved and your incremental extract had no reason to refetch it.</p>



<h3 class="wp-block-heading">Extract fails partway on a large object</h3>



<p class="wp-block-paragraph">Query timeouts or governor limits. Chunk by date range or by ID range rather than pulling the object in one go, and make each chunk independently retryable so a failure costs you one slice rather than the whole run.</p>



<h3 class="wp-block-heading">New Salesforce fields never appear</h3>



<p class="wp-block-paragraph">Most connectors map fields explicitly at configuration time and do not add new ones on their own. Poll the object&#8217;s describe metadata on a schedule and alert when the field list changes, so schema drift is a notification rather than a discovery.</p>



<h3 class="wp-block-heading">Athena queries are slow or expensive</h3>



<p class="wp-block-paragraph">You are scanning too much. Check partitioning first, then whether the files are Parquet rather than JSON or CSV, then whether many tiny files are being read per query. Small-file proliferation is the usual culprit on frequently-run extracts, and compaction fixes it.</p>



<h2 class="wp-block-heading">Common mistakes</h2>



<ul class="wp-block-list">
<li>Building incremental sync on <code>SystemModstamp</code> alone and never handling deletes.</li>
<li>Assuming the Recycle Bin gives you a guaranteed fifteen-day detection window.</li>
<li>Treating CDC as sufficient on its own, with no reconciliation behind it.</li>
<li>Ignoring merges, so a merged customer is counted twice forever.</li>
<li>Extracting formula fields and treating them as durable values.</li>
<li>Nightly full extracts of large objects, and the API outage that follows.</li>
<li>Sharing an integration user with other tools, so nobody can attribute API consumption.</li>
<li>Mutating the raw layer, which destroys your ability to rebuild.</li>
<li>Clicking AppFlow flows together in the console with no infrastructure as code.</li>
<li>Landing JSON or CSV instead of Parquet, then paying for it on every Athena query.</li>
<li>No alerting on schema drift, so new fields are found by a user asking.</li>
<li>Replicating Contacts and Leads into S3 with no retention policy or access controls.</li>
</ul>



<h2 class="wp-block-heading">Best practices</h2>



<ul class="wp-block-list">
<li>Append-only raw layer, curated views on top, and never edit raw.</li>
<li>Use at least two delete mechanisms, one of which is periodic full ID reconciliation.</li>
<li>Soft-delete in curated rather than hard-deleting, so the audit trail survives.</li>
<li>Reimplement formula logic downstream instead of trusting extracted formula values.</li>
<li>Bulk API for extracts, incremental by default.</li>
<li>A dedicated integration user with a documented, deliberately scoped profile.</li>
<li>Partition by extraction date and store Parquet, or use Iceberg if you need row-level updates.</li>
<li>Define flows, catalogs and permissions as code.</li>
<li>Monitor API consumption and row-count delta against Salesforce as first-class metrics.</li>
<li>Alert on schema changes rather than discovering them.</li>
<li>Encrypt, restrict and set a retention policy on personal data the day you land it, not later.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">FAQ</h2>



<h3 class="wp-block-heading">How do I handle deleted Salesforce records in a data lake?</h3>



<p class="wp-block-paragraph">Combine mechanisms. Query the Recycle Bin with <code>queryAll</code> and <code>IsDeleted</code> for recent deletions, consume Change Data Capture events for real-time coverage, and run a periodic full ID reconciliation as the backstop. No single one of the three is complete on its own.</p>



<h3 class="wp-block-heading">Should I use AppFlow or a third-party ELT tool?</h3>



<p class="wp-block-paragraph">AppFlow if you want to stay inside AWS, value PrivateLink connectivity, and are willing to handle schema drift and deletes yourself. A managed vendor if you would rather buy those solved and can live with per-row pricing. Price both against the engineering time honestly; the build-it-yourself option is usually costed at zero and is not.</p>



<h3 class="wp-block-heading">Parquet or Iceberg?</h3>



<p class="wp-block-paragraph">Parquet with date partitions is fine for append-only raw. Iceberg earns its complexity in the curated layer, where you want row-level updates and deletes, schema evolution and time travel. Salesforce data changes constantly, so that need is real rather than theoretical.</p>



<h3 class="wp-block-heading">How often should I sync?</h3>



<p class="wp-block-paragraph">Match the decisions people actually make with the data. Hourly incrementals suit most reporting; sub-minute freshness usually means CDC and a real streaming consumer, which is a much larger commitment. Schedule the ID reconciliation separately and less often, weekly is normally enough.</p>



<h3 class="wp-block-heading">Will this use up my Salesforce API limit?</h3>



<p class="wp-block-paragraph">It can, and the failure is shared with every other integration on the org. Use the Bulk API, stay incremental, give the pipeline its own user so consumption is attributable, and graph usage. Do not find out from an outage.</p>



<h3 class="wp-block-heading">Do I need Salesforce Data Cloud?</h3>



<p class="wp-block-paragraph">Not to build a lake on AWS. It becomes interesting when compliance objects to copying CRM data at all, since zero-copy sharing avoids replication. Check what your existing Salesforce agreement covers before designing around it, because the licensing is a real factor.</p>



<h3 class="wp-block-heading">Why do my numbers differ from Salesforce reports?</h3>



<p class="wp-block-paragraph">Check in this order: missing deletes, merged records counted twice, stale formula fields, fields the integration user cannot see, and only then your own transformation logic. The first two account for most of it, and the last is where people look first.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The one thing to remember</h2>



<p class="wp-block-paragraph">Getting Salesforce data into S3 is a configuration exercise you can finish in an afternoon. Keeping that data equal to Salesforce is an ongoing engineering problem, and the parts that break do not raise errors. They produce numbers that are slightly wrong, in a consistent direction, for months.</p>



<p class="wp-block-paragraph">So build the reconciliation before you build the dashboards. A weekly job that pulls every ID and diffs it against the lake is unglamorous, cheap, and the only thing that will tell you the truth when someone asks why the two systems disagree.</p>



<h2 class="wp-block-heading">Building or fixing one of these?</h2>



<p class="wp-block-paragraph">Most of the Salesforce lakes I get asked to look at work correctly for the first quarter and then quietly diverge. Work I take on:</p>



<ul class="wp-block-list">
<li>Building a Salesforce to S3 pipeline end to end: AppFlow or Bulk API ingestion, Glue catalog, Athena or Redshift Spectrum query layer.</li>
<li>Auditing an existing lake for drift, and reporting exactly how many rows disagree with Salesforce and why.</li>
<li>Implementing proper delete handling: CDC consumption, Recycle Bin queries and scheduled ID reconciliation.</li>
<li>Reworking formula-derived columns into version-controlled transformations that stay correct.</li>
<li>API budget work: moving extracts to Bulk, splitting integration users, and monitoring consumption before it causes an outage.</li>
<li>Access and retention design for CRM data in S3, including encryption, Lake Formation grants and deletion policies.</li>
</ul>



<p class="wp-block-paragraph">Tell me which objects you replicate and how you currently detect deletes, and I will tell you where the drift is coming from.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/devops/salesforce-data-lake-aws/">The Row Count That Only Goes Up: Building a Salesforce Data Lake on AWS</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://john-nessime.com/blog/devops/salesforce-data-lake-aws/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
